diff --git a/.ci/azure-pipelines-build.yml b/.ci/azure-pipelines-build.yml index 9c3a51c9fc..d873172396 100644 --- a/.ci/azure-pipelines-build.yml +++ b/.ci/azure-pipelines-build.yml @@ -16,7 +16,7 @@ jobs: - task: NodeTool@0 displayName: 'Install Node' inputs: - versionSpec: '16.x' + versionSpec: '20.x' - task: Cache@2 displayName: 'Cache node_modules' diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..4f66115614 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,23 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node +{ + "name": "Node.js", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/javascript-node:1-20-bullseye", + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + //https://github.com/microsoft/vscode-dev-containers/issues/559 + "postCreateCommand": "source $NVM_DIR/nvm.sh && nvm install 20" + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.eslintrc.js b/.eslintrc.js index 7b94a83ed3..4caf5f2b93 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -66,6 +66,7 @@ module.exports = { 'no-unused-expressions': ['off'], '@typescript-eslint/no-unused-expressions': ['error', { 'allowShortCircuit': true, 'allowTernary': true, 'allowTaggedTemplates': true }], 'no-unused-private-class-members': ['error'], + 'no-useless-rename': ['error'], 'no-useless-constructor': ['off'], '@typescript-eslint/no-useless-constructor': ['error'], 'no-var': ['error'], diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 085e9d7893..b754665bca 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -18,7 +18,7 @@ jobs: - name: Setup node environment uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 with: - node-version: 16 + node-version: 20 check-latest: true cache: npm @@ -42,7 +42,7 @@ jobs: - name: Setup node environment uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 with: - node-version: 16 + node-version: 20 check-latest: true cache: npm @@ -63,7 +63,7 @@ jobs: - name: Setup node environment uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 with: - node-version: 16 + node-version: 20 check-latest: true cache: npm @@ -87,7 +87,7 @@ jobs: - name: Setup node environment uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 with: - node-version: 16 + node-version: 20 check-latest: true cache: npm diff --git a/.github/workflows/tsc.yml b/.github/workflows/tsc.yml index 2d30664b6f..35bde340f9 100644 --- a/.github/workflows/tsc.yml +++ b/.github/workflows/tsc.yml @@ -18,7 +18,7 @@ jobs: - name: Setup node environment uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 with: - node-version: 16 + node-version: 20 check-latest: true cache: npm @@ -27,3 +27,6 @@ jobs: - name: Run tsc run: npm run build:check + + - name: Run test suite + run: npm run test diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 3cb25b462e..460054108c 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -67,6 +67,7 @@ - [sleepycatcoding](https://github.com/sleepycatcoding) - [TheMelmacian](https://github.com/TheMelmacian) - [v0idMrK](https://github.com/v0idMrK) + - [tehciolo](https://github.com/tehciolo) # Emby Contributors diff --git a/README.md b/README.md index bcb0c53311..2516be0a25 100644 --- a/README.md +++ b/README.md @@ -77,8 +77,9 @@ Jellyfin Web is the frontend used for most of the clients available for end user . └── src ├── apps - │   ├── experimental # New experimental app layout - │   └── stable # Classic (stable) app layout + │   ├── dashboard # Admin dashboard app layout and routes + │   ├── experimental # New experimental app layout and routes + │   └── stable # Classic (stable) app layout and routes ├── assets # Static assets ├── components # Higher order visual components and React components ├── controllers # Legacy page views and controllers 🧹 @@ -87,7 +88,6 @@ Jellyfin Web is the frontend used for most of the clients available for end user ├── legacy # Polyfills for legacy browsers ├── libraries # Third party libraries 🧹 ├── plugins # Client plugins - ├── routes # React routes/pages ├── scripts # Random assortment of visual components and utilities 🐉 ├── strings # Translation files ├── styles # Common app Sass stylesheets diff --git a/deployment/Dockerfile.centos b/deployment/Dockerfile.centos index 566180ad22..32ade8de43 100644 --- a/deployment/Dockerfile.centos +++ b/deployment/Dockerfile.centos @@ -13,8 +13,8 @@ ENV IS_DOCKER=YES RUN yum update -y \ && yum install -y epel-release \ && yum install -y rpmdevtools git autoconf automake glibc-devel gcc-c++ make \ - && curl -fsSL https://rpm.nodesource.com/setup_16.x | bash - \ - && yum install -y nodejs + && yum install https://rpm.nodesource.com/pub_20.x/nodistro/repo/nodesource-release-nodistro-1.noarch.rpm -y \ + && yum install nodejs -y --setopt=nodesource-nodejs.module_hotfixes=1 # Link to build script RUN ln -sf ${SOURCE_DIR}/deployment/build.centos /build.sh diff --git a/deployment/Dockerfile.debian b/deployment/Dockerfile.debian index 1d39d5b591..d42b812bf3 100644 --- a/deployment/Dockerfile.debian +++ b/deployment/Dockerfile.debian @@ -12,11 +12,13 @@ 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_16.x | bash - \ + && apt-get install -y debhelper mmv git curl gnupg ca-certificates \ + && mkdir -p /etc/apt/keyrings \ + && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ + && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \ + && apt-get update \ && apt-get install -y nodejs - # Link to build script RUN ln -sf ${SOURCE_DIR}/deployment/build.debian /build.sh diff --git a/deployment/Dockerfile.docker b/deployment/Dockerfile.docker index 5605e1150f..bd5a939962 100644 --- a/deployment/Dockerfile.docker +++ b/deployment/Dockerfile.docker @@ -1,4 +1,4 @@ -FROM node:lts-alpine +FROM node:20-alpine ARG SOURCE_DIR=/src ARG ARTIFACT_DIR=/jellyfin-web diff --git a/deployment/Dockerfile.fedora b/deployment/Dockerfile.fedora index 5e44024f2e..8c77fae0cf 100644 --- a/deployment/Dockerfile.fedora +++ b/deployment/Dockerfile.fedora @@ -11,7 +11,8 @@ ENV IS_DOCKER=YES # Prepare Fedora environment RUN dnf update -y \ - && dnf install -y @buildsys-build rpmdevtools git dnf-plugins-core nodejs autoconf automake glibc-devel make + && yum install https://rpm.nodesource.com/pub_20.x/nodistro/repo/nodesource-release-nodistro-1.noarch.rpm -y \ + && dnf install -y @buildsys-build rpmdevtools git dnf-plugins-core nodejs autoconf automake glibc-devel make --setopt=nodesource-nodejs.module_hotfixes=1 # Link to build script RUN ln -sf ${SOURCE_DIR}/deployment/build.fedora /build.sh diff --git a/deployment/Dockerfile.portable b/deployment/Dockerfile.portable index 7044cca623..e57052178b 100644 --- a/deployment/Dockerfile.portable +++ b/deployment/Dockerfile.portable @@ -11,8 +11,11 @@ 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_16.x | bash - \ + && apt-get install -y mmv curl git gnupg ca-certificates \ + && mkdir -p /etc/apt/keyrings \ + && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ + && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \ + && apt-get update \ && apt-get install -y nodejs # Link to build script diff --git a/fedora/Makefile b/fedora/Makefile index c094073bc8..adc5ecf672 100644 --- a/fedora/Makefile +++ b/fedora/Makefile @@ -7,7 +7,7 @@ RELEASE := $(shell set -x; sed -ne '/^Release:/s/.* *\(.*\)%{.*}.*/\1/p' $(DIR) SRPM := jellyfin-web-$(subst -,~,$(VERSION))-$(RELEASE)$(shell rpm --eval %dist).src.rpm TARBALL :=$(NAME)-$(subst -,~,$(VERSION)).tar.gz -epel-7-x86_64_repos := https://rpm.nodesource.com/pub_16.x/el/\$$releasever/\$$basearch/ +epel-7-x86_64_repos := https://rpm.nodesource.com/pub_20.x/nodistro/\$$basearch/ fed_ver := $(shell rpm -E %fedora) # fallback when not running on Fedora diff --git a/fedora/jellyfin-web.spec b/fedora/jellyfin-web.spec index 595ef33f2c..fa1c1722e9 100644 --- a/fedora/jellyfin-web.spec +++ b/fedora/jellyfin-web.spec @@ -14,10 +14,10 @@ BuildArch: noarch BuildRequires: nodejs %else BuildRequires: git -# Nodejs 16 is required and npm >= 8 should bring in NodeJS 16 -# This requires the build environment to use the nodejs:16 module stream: -# dnf module {install|switch-to}:web nodejs:16 -BuildRequires: npm >= 8 +# Nodejs 20 is required and npm >= 10 should bring in NodeJS 20 +# This requires the build environment to use the nodejs:20 module stream: +# dnf module {install|switch-to}:web nodejs:20 +BuildRequires: npm >= 10 %endif %description diff --git a/package-lock.json b/package-lock.json index 2ecbd9d155..f2a76ca971 100644 --- a/package-lock.json +++ b/package-lock.json @@ -116,6 +116,7 @@ "stylelint-scss": "5.0.0", "ts-loader": "9.4.4", "typescript": "5.0.4", + "vitest": "0.34.6", "webpack": "5.88.1", "webpack-bundle-analyzer": "4.9.1", "webpack-cli": "5.1.4", @@ -125,8 +126,8 @@ "worker-loader": "3.0.8" }, "engines": { - "node": ">=16.13.1", - "npm": ">=8.1.2", + "node": ">=20.0.0", + "npm": ">=9.6.4", "yarn": "YARN NO LONGER USED - use npm instead." } }, @@ -2719,6 +2720,358 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -2926,6 +3279,18 @@ "axios": "^1.3.4" } }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", @@ -3531,6 +3896,12 @@ "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", "dev": true }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, "node_modules/@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", @@ -3646,6 +4017,21 @@ "@types/node": "*" } }, + "node_modules/@types/chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-VOVRLM1mBxIRxydiViqPcKn6MIxZytrbMpd6RJLIWKxUNr3zux8no0Oc7kJx0WAPIitgZ0gkrDS+btlqQpubpw==", + "dev": true + }, + "node_modules/@types/chai-subset": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz", + "integrity": "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==", + "dev": true, + "dependencies": { + "@types/chai": "*" + } + }, "node_modules/@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -4241,6 +4627,119 @@ "integrity": "sha512-V3vzdXunOKKob1F+2ldv/4iGQoQA/iyqtW8PVlr1v16xCCKL831pGUubT+vs5/9wxTE/zBKEhjIjmmpK6nqw2A==", "dev": true }, + "node_modules/@vitest/expect": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.6.tgz", + "integrity": "sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==", + "dev": true, + "dependencies": { + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.6.tgz", + "integrity": "sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==", + "dev": true, + "dependencies": { + "@vitest/utils": "0.34.6", + "p-limit": "^4.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/runner/node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/snapshot": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.6.tgz", + "integrity": "sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==", + "dev": true, + "dependencies": { + "magic-string": "^0.30.1", + "pathe": "^1.1.1", + "pretty-format": "^29.5.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@vitest/snapshot/node_modules/magic-string": { + "version": "0.30.4", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.4.tgz", + "integrity": "sha512-Q/TKtsC5BPm0kGqgBIF9oXAs/xEf2vRKiIB4wCRQTJOQIByZ1d+NnUOotvJOvNpi5RNIgVOMC3pOuaP1ZTDlVg==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@vitest/spy": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.6.tgz", + "integrity": "sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==", + "dev": true, + "dependencies": { + "tinyspy": "^2.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.6.tgz", + "integrity": "sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==", + "dev": true, + "dependencies": { + "diff-sequences": "^29.4.3", + "loupe": "^2.3.6", + "pretty-format": "^29.5.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", @@ -4817,6 +5316,15 @@ "node": ">=0.10.0" } }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -5399,6 +5907,15 @@ "node": ">= 0.8" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", @@ -5570,6 +6087,24 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chai": { + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", + "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -5623,6 +6158,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -6654,6 +7201,18 @@ "node": ">=8" } }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/deep-equal": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.0.tgz", @@ -6858,6 +7417,15 @@ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "dev": true }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -7390,6 +7958,43 @@ "ext": "^1.1.2" } }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -9300,6 +9905,15 @@ "node": ">=6.9.0" } }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/get-intrinsic": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", @@ -11083,6 +11697,12 @@ "node": ">=6" } }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -11262,6 +11882,18 @@ "node": ">=8.9.0" } }, + "node_modules/local-pkg": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/localforage": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", @@ -11405,6 +12037,15 @@ "node": ">=0.10.0" } }, + "node_modules/loupe": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", + "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.0" + } + }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -11882,6 +12523,30 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mlly": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz", + "integrity": "sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==", + "dev": true, + "dependencies": { + "acorn": "^8.10.0", + "pathe": "^1.1.1", + "pkg-types": "^1.0.3", + "ufo": "^1.3.0" + } + }, + "node_modules/mlly/node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/mrmime": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", @@ -12669,6 +13334,21 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz", + "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==", + "dev": true + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/pdfjs-dist": { "version": "3.6.172", "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.6.172.tgz", @@ -12744,6 +13424,17 @@ "node": ">=8" } }, + "node_modules/pkg-types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", + "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", + "dev": true, + "dependencies": { + "jsonc-parser": "^3.2.0", + "mlly": "^1.2.0", + "pathe": "^1.1.0" + } + }, "node_modules/plur": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/plur/-/plur-2.1.2.tgz", @@ -14465,6 +15156,38 @@ "renderkid": "^3.0.0" } }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -15598,6 +16321,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, "node_modules/signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", @@ -16183,6 +16912,12 @@ "node": "*" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, "node_modules/state-toggle": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz", @@ -16227,6 +16962,12 @@ "node": ">= 0.6" } }, + "node_modules/std-env": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.4.3.tgz", + "integrity": "sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==", + "dev": true + }, "node_modules/stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", @@ -16415,6 +17156,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz", + "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==", + "dev": true, + "dependencies": { + "acorn": "^8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/style-loader": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz", @@ -19397,6 +20162,30 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, + "node_modules/tinybench": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.1.tgz", + "integrity": "sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==", + "dev": true + }, + "node_modules/tinypool": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz", + "integrity": "sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz", + "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -19702,6 +20491,15 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -19740,6 +20538,12 @@ "node": ">=12.20" } }, + "node_modules/ufo": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.1.tgz", + "integrity": "sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw==", + "dev": true + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -20105,6 +20909,235 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/vite": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", + "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", + "dev": true, + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.6.tgz", + "integrity": "sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "mlly": "^1.4.0", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": ">=v14.18.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/vite/node_modules/rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/vitest": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz", + "integrity": "sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==", + "dev": true, + "dependencies": { + "@types/chai": "^4.3.5", + "@types/chai-subset": "^1.3.3", + "@types/node": "*", + "@vitest/expect": "0.34.6", + "@vitest/runner": "0.34.6", + "@vitest/snapshot": "0.34.6", + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "acorn": "^8.9.0", + "acorn-walk": "^8.2.0", + "cac": "^6.7.14", + "chai": "^4.3.10", + "debug": "^4.3.4", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.1", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.3.3", + "strip-literal": "^1.0.1", + "tinybench": "^2.5.0", + "tinypool": "^0.7.0", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", + "vite-node": "0.34.6", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": ">=v14.18.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@vitest/browser": "*", + "@vitest/ui": "*", + "happy-dom": "*", + "jsdom": "*", + "playwright": "*", + "safaridriver": "*", + "webdriverio": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "playwright": { + "optional": true + }, + "safaridriver": { + "optional": true + }, + "webdriverio": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/vitest/node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/vitest/node_modules/magic-string": { + "version": "0.30.4", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.4.tgz", + "integrity": "sha512-Q/TKtsC5BPm0kGqgBIF9oXAs/xEf2vRKiIB4wCRQTJOQIByZ1d+NnUOotvJOvNpi5RNIgVOMC3pOuaP1ZTDlVg==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -20628,6 +21661,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", + "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", @@ -22800,6 +23849,160 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" }, + "@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "dev": true, + "optional": true + }, "@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -22957,6 +24160,15 @@ "integrity": "sha512-1+GXATaJLP5akFnUrpxYzoshLtTPZXJEdy/ozhY1g/DkULlz4LthLTaTJ2qImF0mb8Ayk7LNbh00n4ATk0JycA==", "requires": {} }, + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, "@jridgewell/gen-mapping": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", @@ -23331,6 +24543,12 @@ } } }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, "@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", @@ -23406,6 +24624,21 @@ "@types/node": "*" } }, + "@types/chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-VOVRLM1mBxIRxydiViqPcKn6MIxZytrbMpd6RJLIWKxUNr3zux8no0Oc7kJx0WAPIitgZ0gkrDS+btlqQpubpw==", + "dev": true + }, + "@types/chai-subset": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz", + "integrity": "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==", + "dev": true, + "requires": { + "@types/chai": "*" + } + }, "@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -23889,6 +25122,93 @@ "integrity": "sha512-V3vzdXunOKKob1F+2ldv/4iGQoQA/iyqtW8PVlr1v16xCCKL831pGUubT+vs5/9wxTE/zBKEhjIjmmpK6nqw2A==", "dev": true }, + "@vitest/expect": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.6.tgz", + "integrity": "sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==", + "dev": true, + "requires": { + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "chai": "^4.3.10" + } + }, + "@vitest/runner": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.6.tgz", + "integrity": "sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==", + "dev": true, + "requires": { + "@vitest/utils": "0.34.6", + "p-limit": "^4.0.0", + "pathe": "^1.1.1" + }, + "dependencies": { + "p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "requires": { + "yocto-queue": "^1.0.0" + } + }, + "yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true + } + } + }, + "@vitest/snapshot": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.6.tgz", + "integrity": "sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==", + "dev": true, + "requires": { + "magic-string": "^0.30.1", + "pathe": "^1.1.1", + "pretty-format": "^29.5.0" + }, + "dependencies": { + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "magic-string": { + "version": "0.30.4", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.4.tgz", + "integrity": "sha512-Q/TKtsC5BPm0kGqgBIF9oXAs/xEf2vRKiIB4wCRQTJOQIByZ1d+NnUOotvJOvNpi5RNIgVOMC3pOuaP1ZTDlVg==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + } + } + }, + "@vitest/spy": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.6.tgz", + "integrity": "sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==", + "dev": true, + "requires": { + "tinyspy": "^2.1.1" + } + }, + "@vitest/utils": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.6.tgz", + "integrity": "sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==", + "dev": true, + "requires": { + "diff-sequences": "^29.4.3", + "loupe": "^2.3.6", + "pretty-format": "^29.5.0" + } + }, "@webassemblyjs/ast": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", @@ -24344,6 +25664,12 @@ "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", "dev": true }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -24783,6 +26109,12 @@ "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", "dev": true }, + "cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true + }, "cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", @@ -24908,6 +26240,21 @@ "integrity": "sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==", "dev": true }, + "chai": { + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", + "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + } + }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -24942,6 +26289,15 @@ "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", "dev": true }, + "check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "requires": { + "get-func-name": "^2.0.2" + } + }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -25681,6 +27037,15 @@ "mimic-response": "^2.0.0" } }, + "deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, "deep-equal": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.0.tgz", @@ -25843,6 +27208,12 @@ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "dev": true }, + "diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true + }, "dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -26274,6 +27645,36 @@ "ext": "^1.1.2" } }, + "esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "requires": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -27715,6 +29116,12 @@ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true }, + "get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true + }, "get-intrinsic": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", @@ -29007,6 +30414,12 @@ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true }, + "jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, "jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -29162,6 +30575,12 @@ "json5": "^2.1.2" } }, + "local-pkg": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "dev": true + }, "localforage": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", @@ -29292,6 +30711,15 @@ "signal-exit": "^3.0.0" } }, + "loupe": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", + "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "dev": true, + "requires": { + "get-func-name": "^2.0.0" + } + }, "lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -29639,6 +31067,26 @@ "minimist": "^1.2.5" } }, + "mlly": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz", + "integrity": "sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==", + "dev": true, + "requires": { + "acorn": "^8.10.0", + "pathe": "^1.1.1", + "pkg-types": "^1.0.3", + "ufo": "^1.3.0" + }, + "dependencies": { + "acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true + } + } + }, "mrmime": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", @@ -30245,6 +31693,18 @@ "resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz", "integrity": "sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==" }, + "pathe": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz", + "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==", + "dev": true + }, + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true + }, "pdfjs-dist": { "version": "3.6.172", "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.6.172.tgz", @@ -30297,6 +31757,17 @@ "find-up": "^4.0.0" } }, + "pkg-types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", + "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", + "dev": true, + "requires": { + "jsonc-parser": "^3.2.0", + "mlly": "^1.2.0", + "pathe": "^1.1.0" + } + }, "plur": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/plur/-/plur-2.1.2.tgz", @@ -31411,6 +32882,31 @@ "renderkid": "^3.0.0" } }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } + } + }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -32277,6 +33773,12 @@ "object-inspect": "^1.9.0" } }, + "siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, "signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", @@ -32750,6 +34252,12 @@ "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", "dev": true }, + "stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, "state-toggle": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz", @@ -32783,6 +34291,12 @@ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", "dev": true }, + "std-env": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.4.3.tgz", + "integrity": "sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==", + "dev": true + }, "stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", @@ -32928,6 +34442,23 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, + "strip-literal": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz", + "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==", + "dev": true, + "requires": { + "acorn": "^8.10.0" + }, + "dependencies": { + "acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true + } + } + }, "style-loader": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz", @@ -35236,6 +36767,24 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, + "tinybench": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.1.tgz", + "integrity": "sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==", + "dev": true + }, + "tinypool": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz", + "integrity": "sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==", + "dev": true + }, + "tinyspy": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz", + "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==", + "dev": true + }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -35471,6 +37020,12 @@ "prelude-ls": "^1.2.1" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -35493,6 +37048,12 @@ "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", "dev": true }, + "ufo": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.1.tgz", + "integrity": "sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw==", + "dev": true + }, "unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -35770,6 +37331,109 @@ "unist-util-stringify-position": "^2.0.0" } }, + "vite": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", + "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", + "dev": true, + "requires": { + "esbuild": "^0.18.10", + "fsevents": "~2.3.2", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "dependencies": { + "postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "requires": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + } + } + }, + "vite-node": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.6.tgz", + "integrity": "sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==", + "dev": true, + "requires": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "mlly": "^1.4.0", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0-0" + } + }, + "vitest": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz", + "integrity": "sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==", + "dev": true, + "requires": { + "@types/chai": "^4.3.5", + "@types/chai-subset": "^1.3.3", + "@types/node": "*", + "@vitest/expect": "0.34.6", + "@vitest/runner": "0.34.6", + "@vitest/snapshot": "0.34.6", + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "acorn": "^8.9.0", + "acorn-walk": "^8.2.0", + "cac": "^6.7.14", + "chai": "^4.3.10", + "debug": "^4.3.4", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.1", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.3.3", + "strip-literal": "^1.0.1", + "tinybench": "^2.5.0", + "tinypool": "^0.7.0", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", + "vite-node": "0.34.6", + "why-is-node-running": "^2.2.2" + }, + "dependencies": { + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true + }, + "magic-string": { + "version": "0.30.4", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.4.tgz", + "integrity": "sha512-Q/TKtsC5BPm0kGqgBIF9oXAs/xEf2vRKiIB4wCRQTJOQIByZ1d+NnUOotvJOvNpi5RNIgVOMC3pOuaP1ZTDlVg==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + } + } + }, "watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -36148,6 +37812,16 @@ "is-typed-array": "^1.1.10" } }, + "why-is-node-running": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", + "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "dev": true, + "requires": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + } + }, "wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", diff --git a/package.json b/package.json index 988c2a0215..cacf693046 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "stylelint-scss": "5.0.0", "ts-loader": "9.4.4", "typescript": "5.0.4", + "vitest": "0.34.6", "webpack": "5.88.1", "webpack-bundle-analyzer": "4.9.1", "webpack-cli": "5.1.4", @@ -145,13 +146,15 @@ "build:check": "tsc --noEmit", "escheck": "es-check", "lint": "eslint \"./\"", + "test": "vitest --watch=false", + "test:watch": "vitest", "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", + "node": ">=20.0.0", + "npm": ">=9.6.4", "yarn": "YARN NO LONGER USED - use npm instead." } } diff --git a/src/RootApp.tsx b/src/RootApp.tsx index 62223f7236..cc10ca7baa 100644 --- a/src/RootApp.tsx +++ b/src/RootApp.tsx @@ -1,37 +1,62 @@ import loadable from '@loadable/component'; +import { ThemeProvider } from '@mui/material/styles'; import { History } from '@remix-run/router'; -import React from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import React from 'react'; +import { useLocation } from 'react-router-dom'; -import StableApp from './apps/stable/App'; -import { HistoryRouter } from './components/router/HistoryRouter'; -import { ApiProvider } from './hooks/useApi'; -import { WebConfigProvider } from './hooks/useWebConfig'; +import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App'; +import AppHeader from 'components/AppHeader'; +import Backdrop from 'components/Backdrop'; +import { HistoryRouter } from 'components/router/HistoryRouter'; +import { ApiProvider } from 'hooks/useApi'; +import { WebConfigProvider } from 'hooks/useWebConfig'; +import theme from 'themes/theme'; +const DashboardApp = loadable(() => import('./apps/dashboard/App')); const ExperimentalApp = loadable(() => import('./apps/experimental/App')); +const StableApp = loadable(() => import('./apps/stable/App')); const queryClient = new QueryClient(); -const RootApp = ({ history }: { history: History }) => { +const RootAppLayout = () => { const layoutMode = localStorage.getItem('layout'); + const isExperimentalLayout = layoutMode === 'experimental'; + + const location = useLocation(); + const isNewLayoutPath = Object.values(DASHBOARD_APP_PATHS) + .some(path => location.pathname.startsWith(`/${path}`)); return ( - - - - - { - layoutMode === 'experimental' ? - : - - } - - - - - + <> + + + + { + isExperimentalLayout ? + : + + } + + + ); }; +const RootApp = ({ history }: { history: History }) => ( + + + + + + + + + + + + +); + export default RootApp; diff --git a/src/apps/dashboard/App.tsx b/src/apps/dashboard/App.tsx new file mode 100644 index 0000000000..c5ab679297 --- /dev/null +++ b/src/apps/dashboard/App.tsx @@ -0,0 +1,66 @@ +import loadable from '@loadable/component'; +import React from 'react'; +import { Route, Routes } from 'react-router-dom'; + +import ConnectionRequired from 'components/ConnectionRequired'; +import { toViewManagerPageRoute } from 'components/router/LegacyRoute'; +import { AsyncPageProps, AsyncRoute, toAsyncPageRoute } from 'components/router/AsyncRoute'; +import { toRedirectRoute } from 'components/router/Redirect'; +import ServerContentPage from 'components/ServerContentPage'; + +import AppLayout from './AppLayout'; +import { REDIRECTS } from './routes/_redirects'; +import { ASYNC_ADMIN_ROUTES } from './routes/_asyncRoutes'; +import { LEGACY_ADMIN_ROUTES } from './routes/_legacyRoutes'; + +const DashboardAsyncPage = loadable( + (props: { page: string }) => import(/* webpackChunkName: "[request]" */ `./routes/${props.page}`), + { cacheKey: (props: AsyncPageProps) => props.page } +); + +const toDashboardAsyncPageRoute = (route: AsyncRoute) => ( + toAsyncPageRoute({ + ...route, + element: DashboardAsyncPage + }) +); + +export const DASHBOARD_APP_PATHS = { + Dashboard: 'dashboard', + MetadataManager: 'metadata', + PluginConfig: 'configurationpage' +}; + +const DashboardApp = () => ( + + }> + }> + + {ASYNC_ADMIN_ROUTES.map(toDashboardAsyncPageRoute)} + {LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)} + + + {/* NOTE: The metadata editor might deserve a dedicated app in the future */} + {toViewManagerPageRoute({ + path: DASHBOARD_APP_PATHS.MetadataManager, + pageProps: { + controller: 'edititemmetadata', + view: 'edititemmetadata.html' + } + })} + + + } /> + + + {/* Suppress warnings for unhandled routes */} + + + + {/* Redirects for old paths */} + {REDIRECTS.map(toRedirectRoute)} + +); + +export default DashboardApp; diff --git a/src/apps/dashboard/AppLayout.tsx b/src/apps/dashboard/AppLayout.tsx new file mode 100644 index 0000000000..ce74f4989b --- /dev/null +++ b/src/apps/dashboard/AppLayout.tsx @@ -0,0 +1,108 @@ +import AppBar from '@mui/material/AppBar'; +import Box from '@mui/material/Box'; +import { useTheme } from '@mui/material/styles'; +import React, { FC, useCallback, useEffect, useState } from 'react'; +import { Outlet, useLocation } from 'react-router-dom'; + +import AppBody from 'components/AppBody'; +import AppToolbar from 'components/toolbar/AppToolbar'; +import ElevationScroll from 'components/ElevationScroll'; +import { DRAWER_WIDTH } from 'components/ResponsiveDrawer'; +import { useApi } from 'hooks/useApi'; +import { useLocalStorage } from 'hooks/useLocalStorage'; + +import AppDrawer from './components/drawer/AppDrawer'; + +import './AppOverrides.scss'; + +interface AppLayoutProps { + drawerlessPaths: string[] +} + +interface DashboardAppSettings { + isDrawerPinned: boolean +} + +const DEFAULT_APP_SETTINGS: DashboardAppSettings = { + isDrawerPinned: false +}; + +const AppLayout: FC = ({ + drawerlessPaths +}) => { + const [ appSettings, setAppSettings ] = useLocalStorage('DashboardAppSettings', DEFAULT_APP_SETTINGS); + const [ isDrawerActive, setIsDrawerActive ] = useState(appSettings.isDrawerPinned); + const location = useLocation(); + const theme = useTheme(); + const { user } = useApi(); + + const isDrawerAvailable = !drawerlessPaths.some(path => location.pathname.startsWith(`/${path}`)); + const isDrawerOpen = isDrawerActive && isDrawerAvailable && Boolean(user); + + useEffect(() => { + if (isDrawerActive !== appSettings.isDrawerPinned) { + setAppSettings({ + ...appSettings, + isDrawerPinned: isDrawerActive + }); + } + }, [ appSettings, isDrawerActive, setAppSettings ]); + + const onToggleDrawer = useCallback(() => { + setIsDrawerActive(!isDrawerActive); + }, [ isDrawerActive, setIsDrawerActive ]); + + return ( + + + muiTheme.zIndex.drawer + 1 }} + > + + + + + + + + + + + + + ); +}; + +export default AppLayout; diff --git a/src/apps/dashboard/AppOverrides.scss b/src/apps/dashboard/AppOverrides.scss new file mode 100644 index 0000000000..c8597ee576 --- /dev/null +++ b/src/apps/dashboard/AppOverrides.scss @@ -0,0 +1,22 @@ +// Default MUI breakpoints +// https://mui.com/material-ui/customization/breakpoints/#default-breakpoints +$mui-bp-sm: 600px; +$mui-bp-md: 900px; +$mui-bp-lg: 1200px; +$mui-bp-xl: 1536px; + +// Fix dashboard pages layout to work with drawer +.dashboardDocument { + .mainAnimatedPage { + position: relative; + } + + .skinBody { + position: unset !important; + } + + // Fix the padding of dashboard pages + .content-primary.content-primary { + padding-top: 3.25rem !important; + } +} diff --git a/src/apps/experimental/components/activityTable/LogLevelChip.tsx b/src/apps/dashboard/components/activityTable/LogLevelChip.tsx similarity index 100% rename from src/apps/experimental/components/activityTable/LogLevelChip.tsx rename to src/apps/dashboard/components/activityTable/LogLevelChip.tsx diff --git a/src/apps/experimental/components/activityTable/OverviewCell.tsx b/src/apps/dashboard/components/activityTable/OverviewCell.tsx similarity index 100% rename from src/apps/experimental/components/activityTable/OverviewCell.tsx rename to src/apps/dashboard/components/activityTable/OverviewCell.tsx diff --git a/src/apps/experimental/components/GridActionsCellLink.tsx b/src/apps/dashboard/components/dataGrid/GridActionsCellLink.tsx similarity index 100% rename from src/apps/experimental/components/GridActionsCellLink.tsx rename to src/apps/dashboard/components/dataGrid/GridActionsCellLink.tsx diff --git a/src/apps/dashboard/components/drawer/AppDrawer.tsx b/src/apps/dashboard/components/drawer/AppDrawer.tsx new file mode 100644 index 0000000000..7b1e180123 --- /dev/null +++ b/src/apps/dashboard/components/drawer/AppDrawer.tsx @@ -0,0 +1,29 @@ +import React, { FC } from 'react'; + +import ResponsiveDrawer, { ResponsiveDrawerProps } from 'components/ResponsiveDrawer'; + +import ServerDrawerSection from './sections/ServerDrawerSection'; +import DevicesDrawerSection from './sections/DevicesDrawerSection'; +import LiveTvDrawerSection from './sections/LiveTvDrawerSection'; +import AdvancedDrawerSection from './sections/AdvancedDrawerSection'; +import PluginDrawerSection from './sections/PluginDrawerSection'; + +const AppDrawer: FC = ({ + open = false, + onClose, + onOpen +}) => ( + + + + + + + +); + +export default AppDrawer; diff --git a/src/apps/experimental/components/drawers/dashboard/AdvancedDrawerSection.tsx b/src/apps/dashboard/components/drawer/sections/AdvancedDrawerSection.tsx similarity index 83% rename from src/apps/experimental/components/drawers/dashboard/AdvancedDrawerSection.tsx rename to src/apps/dashboard/components/drawer/sections/AdvancedDrawerSection.tsx index 7e0bb4e366..97993a76cc 100644 --- a/src/apps/experimental/components/drawers/dashboard/AdvancedDrawerSection.tsx +++ b/src/apps/dashboard/components/drawer/sections/AdvancedDrawerSection.tsx @@ -15,15 +15,14 @@ import ListSubheader from '@mui/material/ListSubheader'; import React from 'react'; import { useLocation } from 'react-router-dom'; +import ListItemLink from 'components/ListItemLink'; import globalize from 'scripts/globalize'; -import ListItemLink from '../ListItemLink'; - const PLUGIN_PATHS = [ - '/installedplugins.html', - '/availableplugins.html', - '/repositories.html', - '/addplugin.html', + '/dashboard/plugins', + '/dashboard/plugins/catalog', + '/dashboard/plugins/repositories', + '/dashboard/plugins/add', '/configurationpage' ]; @@ -42,7 +41,7 @@ const AdvancedDrawerSection = () => { } > - + @@ -50,7 +49,7 @@ const AdvancedDrawerSection = () => { - + @@ -58,7 +57,7 @@ const AdvancedDrawerSection = () => { - +
@@ -66,7 +65,7 @@ const AdvancedDrawerSection = () => { - + @@ -74,7 +73,7 @@ const AdvancedDrawerSection = () => { - + @@ -84,19 +83,19 @@ const AdvancedDrawerSection = () => { - + - + - + - + diff --git a/src/apps/experimental/components/drawers/dashboard/DevicesDrawerSection.tsx b/src/apps/dashboard/components/drawer/sections/DevicesDrawerSection.tsx similarity index 86% rename from src/apps/experimental/components/drawers/dashboard/DevicesDrawerSection.tsx rename to src/apps/dashboard/components/drawer/sections/DevicesDrawerSection.tsx index cb3cbf33ce..6cc7ab79fc 100644 --- a/src/apps/experimental/components/drawers/dashboard/DevicesDrawerSection.tsx +++ b/src/apps/dashboard/components/drawer/sections/DevicesDrawerSection.tsx @@ -8,13 +8,12 @@ import ListSubheader from '@mui/material/ListSubheader'; import React from 'react'; import { useLocation } from 'react-router-dom'; +import ListItemLink from 'components/ListItemLink'; import globalize from 'scripts/globalize'; -import ListItemLink from '../ListItemLink'; - const DLNA_PATHS = [ - '/dlnasettings.html', - '/dlnaprofiles.html' + '/dashboard/dlna', + '/dashboard/dlna/profiles' ]; const DevicesDrawerSection = () => { @@ -32,7 +31,7 @@ const DevicesDrawerSection = () => { } > - + @@ -48,7 +47,7 @@ const DevicesDrawerSection = () => { - + @@ -58,10 +57,10 @@ const DevicesDrawerSection = () => { - + - + diff --git a/src/apps/experimental/components/drawers/dashboard/LiveTvDrawerSection.tsx b/src/apps/dashboard/components/drawer/sections/LiveTvDrawerSection.tsx similarity index 88% rename from src/apps/experimental/components/drawers/dashboard/LiveTvDrawerSection.tsx rename to src/apps/dashboard/components/drawer/sections/LiveTvDrawerSection.tsx index 505973b82c..35ea15ce0d 100644 --- a/src/apps/experimental/components/drawers/dashboard/LiveTvDrawerSection.tsx +++ b/src/apps/dashboard/components/drawer/sections/LiveTvDrawerSection.tsx @@ -6,10 +6,9 @@ import ListItemText from '@mui/material/ListItemText'; import ListSubheader from '@mui/material/ListSubheader'; import React from 'react'; +import ListItemLink from 'components/ListItemLink'; import globalize from 'scripts/globalize'; -import ListItemLink from '../ListItemLink'; - const LiveTvDrawerSection = () => { return ( { } > - + @@ -29,7 +28,7 @@ const LiveTvDrawerSection = () => { - + diff --git a/src/apps/experimental/components/drawers/dashboard/PluginDrawerSection.tsx b/src/apps/dashboard/components/drawer/sections/PluginDrawerSection.tsx similarity index 97% rename from src/apps/experimental/components/drawers/dashboard/PluginDrawerSection.tsx rename to src/apps/dashboard/components/drawer/sections/PluginDrawerSection.tsx index 8a368af8d5..fe1ac2613b 100644 --- a/src/apps/experimental/components/drawers/dashboard/PluginDrawerSection.tsx +++ b/src/apps/dashboard/components/drawer/sections/PluginDrawerSection.tsx @@ -8,12 +8,11 @@ import ListItemText from '@mui/material/ListItemText'; import ListSubheader from '@mui/material/ListSubheader'; import React, { useEffect, useState } from 'react'; +import ListItemLink from 'components/ListItemLink'; import { useApi } from 'hooks/useApi'; import globalize from 'scripts/globalize'; import Dashboard from 'utils/dashboard'; -import ListItemLink from '../ListItemLink'; - const PluginDrawerSection = () => { const { api } = useApi(); const [ pagesInfo, setPagesInfo ] = useState([]); diff --git a/src/apps/experimental/components/drawers/dashboard/ServerDrawerSection.tsx b/src/apps/dashboard/components/drawer/sections/ServerDrawerSection.tsx similarity index 77% rename from src/apps/experimental/components/drawers/dashboard/ServerDrawerSection.tsx rename to src/apps/dashboard/components/drawer/sections/ServerDrawerSection.tsx index 388c1feeeb..01e26ace84 100644 --- a/src/apps/experimental/components/drawers/dashboard/ServerDrawerSection.tsx +++ b/src/apps/dashboard/components/drawer/sections/ServerDrawerSection.tsx @@ -8,21 +8,20 @@ import ListSubheader from '@mui/material/ListSubheader'; import React from 'react'; import { useLocation } from 'react-router-dom'; +import ListItemLink from 'components/ListItemLink'; import globalize from 'scripts/globalize'; -import ListItemLink from '../ListItemLink'; - const LIBRARY_PATHS = [ - '/library.html', - '/librarydisplay.html', - '/metadataimages.html', - '/metadatanfo.html' + '/dashboard/libraries', + '/dashboard/libraries/display', + '/dashboard/libraries/metadata', + '/dashboard/libraries/nfo' ]; const PLAYBACK_PATHS = [ - '/encodingsettings.html', - '/playbackconfiguration.html', - '/streamingsettings.html' + '/dashboard/playback/transcoding', + '/dashboard/playback/resume', + '/dashboard/playback/streaming' ]; const ServerDrawerSection = () => { @@ -41,7 +40,7 @@ const ServerDrawerSection = () => { } > - + @@ -49,7 +48,7 @@ const ServerDrawerSection = () => { - + @@ -57,7 +56,7 @@ const ServerDrawerSection = () => { - + @@ -65,7 +64,7 @@ const ServerDrawerSection = () => { - + @@ -75,22 +74,22 @@ const ServerDrawerSection = () => { - + - + - + - + - + @@ -100,13 +99,13 @@ const ServerDrawerSection = () => { - + - + - + diff --git a/src/apps/dashboard/routes/_asyncRoutes.ts b/src/apps/dashboard/routes/_asyncRoutes.ts new file mode 100644 index 0000000000..09d40de0e8 --- /dev/null +++ b/src/apps/dashboard/routes/_asyncRoutes.ts @@ -0,0 +1,12 @@ +import type { AsyncRoute } from 'components/router/AsyncRoute'; + +export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [ + { path: 'activity' }, + { path: 'notifications' }, + { path: 'users' }, + { path: 'users/access' }, + { path: 'users/add' }, + { path: 'users/parentalcontrol' }, + { path: 'users/password' }, + { path: 'users/profile' } +]; diff --git a/src/apps/experimental/routes/legacyRoutes/admin.ts b/src/apps/dashboard/routes/_legacyRoutes.ts similarity index 73% rename from src/apps/experimental/routes/legacyRoutes/admin.ts rename to src/apps/dashboard/routes/_legacyRoutes.ts index b53f86ce87..efdd543a42 100644 --- a/src/apps/experimental/routes/legacyRoutes/admin.ts +++ b/src/apps/dashboard/routes/_legacyRoutes.ts @@ -1,176 +1,164 @@ -import { LegacyRoute } from '../../../../components/router/LegacyRoute'; +import type { LegacyRoute } from 'components/router/LegacyRoute'; export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [ { - path: 'dashboard.html', + path: '/dashboard', pageProps: { controller: 'dashboard/dashboard', view: 'dashboard/dashboard.html' } }, { - path: 'dashboardgeneral.html', + path: 'settings', pageProps: { controller: 'dashboard/general', view: 'dashboard/general.html' } }, { - path: 'networking.html', + path: 'networking', pageProps: { controller: 'dashboard/networking', view: 'dashboard/networking.html' } }, { - path: 'devices.html', + path: 'devices', pageProps: { controller: 'dashboard/devices/devices', view: 'dashboard/devices/devices.html' } }, { - path: 'device.html', + path: 'devices/edit', pageProps: { controller: 'dashboard/devices/device', view: 'dashboard/devices/device.html' } }, { - path: 'quickConnect.html', - pageProps: { - controller: 'dashboard/quickConnect', - view: 'dashboard/quickConnect.html' - } - }, { - path: 'dlnaprofile.html', + path: 'dlna/profiles/edit', pageProps: { controller: 'dashboard/dlna/profile', view: 'dashboard/dlna/profile.html' } }, { - path: 'dlnaprofiles.html', + path: 'dlna/profiles', pageProps: { controller: 'dashboard/dlna/profiles', view: 'dashboard/dlna/profiles.html' } }, { - path: 'dlnasettings.html', + path: 'dlna', pageProps: { controller: 'dashboard/dlna/settings', view: 'dashboard/dlna/settings.html' } }, { - path: 'addplugin.html', + path: 'plugins/add', pageProps: { controller: 'dashboard/plugins/add/index', view: 'dashboard/plugins/add/index.html' } }, { - path: 'library.html', + path: 'libraries', pageProps: { controller: 'dashboard/library', view: 'dashboard/library.html' } }, { - path: 'librarydisplay.html', + path: 'libraries/display', pageProps: { controller: 'dashboard/librarydisplay', view: 'dashboard/librarydisplay.html' } }, { - path: 'edititemmetadata.html', - pageProps: { - controller: 'edititemmetadata', - view: 'edititemmetadata.html' - } - }, { - path: 'encodingsettings.html', + path: 'playback/transcoding', pageProps: { controller: 'dashboard/encodingsettings', view: 'dashboard/encodingsettings.html' } }, { - path: 'log.html', + path: 'logs', pageProps: { controller: 'dashboard/logs', view: 'dashboard/logs.html' } }, { - path: 'metadataimages.html', + path: 'libraries/metadata', pageProps: { controller: 'dashboard/metadataImages', view: 'dashboard/metadataimages.html' } }, { - path: 'metadatanfo.html', + path: 'libraries/nfo', pageProps: { controller: 'dashboard/metadatanfo', view: 'dashboard/metadatanfo.html' } }, { - path: 'playbackconfiguration.html', + path: 'playback/resume', pageProps: { controller: 'dashboard/playback', view: 'dashboard/playback.html' } }, { - path: 'availableplugins.html', + path: 'plugins/catalog', pageProps: { controller: 'dashboard/plugins/available/index', view: 'dashboard/plugins/available/index.html' } }, { - path: 'repositories.html', + path: 'plugins/repositories', pageProps: { controller: 'dashboard/plugins/repositories/index', view: 'dashboard/plugins/repositories/index.html' } }, { - path: 'livetvguideprovider.html', + path: 'livetv/guide', pageProps: { controller: 'livetvguideprovider', view: 'livetvguideprovider.html' } }, { - path: 'livetvsettings.html', + path: 'recordings', pageProps: { controller: 'livetvsettings', view: 'livetvsettings.html' } }, { - path: 'livetvstatus.html', + path: 'livetv', pageProps: { controller: 'livetvstatus', view: 'livetvstatus.html' } }, { - path: 'livetvtuner.html', + path: 'livetv/tuner', pageProps: { controller: 'livetvtuner', view: 'livetvtuner.html' } }, { - path: 'installedplugins.html', + path: 'plugins', pageProps: { controller: 'dashboard/plugins/installed/index', view: 'dashboard/plugins/installed/index.html' } }, { - path: 'scheduledtask.html', + path: 'tasks/edit', pageProps: { controller: 'dashboard/scheduledtasks/scheduledtask', view: 'dashboard/scheduledtasks/scheduledtask.html' } }, { - path: 'scheduledtasks.html', + path: 'tasks', pageProps: { controller: 'dashboard/scheduledtasks/scheduledtasks', view: 'dashboard/scheduledtasks/scheduledtasks.html' } }, { - path: 'apikeys.html', + path: 'keys', pageProps: { controller: 'dashboard/apikeys', view: 'dashboard/apikeys.html' } }, { - path: 'streamingsettings.html', + path: 'playback/streaming', pageProps: { view: 'dashboard/streaming.html', controller: 'dashboard/streaming' diff --git a/src/apps/dashboard/routes/_redirects.ts b/src/apps/dashboard/routes/_redirects.ts new file mode 100644 index 0000000000..94211c79c2 --- /dev/null +++ b/src/apps/dashboard/routes/_redirects.ts @@ -0,0 +1,40 @@ +import type { Redirect } from 'components/router/Redirect'; + +export const REDIRECTS: Redirect[] = [ + { from: 'addplugin.html', to: '/dashboard/plugins/add' }, + { from: 'apikeys.html', to: '/dashboard/keys' }, + { from: 'availableplugins.html', to: '/dashboard/plugins/catalog' }, + { from: 'dashboard.html', to: '/dashboard' }, + { from: 'dashboardgeneral.html', to: '/dashboard/settings' }, + { from: 'device.html', to: '/dashboard/devices/edit' }, + { from: 'devices.html', to: '/dashboard/devices' }, + { from: 'dlnaprofile.html', to: '/dashboard/dlna/profiles/edit' }, + { from: 'dlnaprofiles.html', to: '/dashboard/dlna/profiles' }, + { from: 'dlnasettings.html', to: '/dashboard/dlna' }, + { from: 'edititemmetadata.html', to: '/metadata' }, + { from: 'encodingsettings.html', to: '/dashboard/playback/transcoding' }, + { from: 'installedplugins.html', to: '/dashboard/plugins' }, + { from: 'library.html', to: '/dashboard/libraries' }, + { from: 'librarydisplay.html', to: '/dashboard/libraries/display' }, + { from: 'livetvguideprovider.html', to: '/dashboard/livetv/guide' }, + { from: 'livetvsettings.html', to: '/dashboard/recordings' }, + { from: 'livetvstatus.html', to: '/dashboard/livetv' }, + { from: 'livetvtuner.html', to: '/dashboard/livetv/tuner' }, + { from: 'log.html', to: '/dashboard/logs' }, + { from: 'metadataimages.html', to: '/dashboard/libraries/metadata' }, + { from: 'metadatanfo.html', to: '/dashboard/libraries/nfo' }, + { from: 'networking.html', to: '/dashboard/networking' }, + { from: 'notificationsettings.html', to: '/dashboard/notifications' }, + { from: 'playbackconfiguration.html', to: '/dashboard/playback/resume' }, + { from: 'repositories.html', to: '/dashboard/plugins/repositories' }, + { from: 'scheduledtask.html', to: '/dashboard/tasks/edit' }, + { from: 'scheduledtasks.html', to: '/dashboard/tasks' }, + { from: 'serveractivity.html', to: '/dashboard/activity' }, + { from: 'streamingsettings.html', to: '/dashboard/playback/streaming' }, + { from: 'useredit.html', to: '/dashboard/users/profile' }, + { from: 'userlibraryaccess.html', to: '/dashboard/users/access' }, + { from: 'usernew.html', to: '/dashboard/users/add' }, + { from: 'userparentalcontrol.html', to: '/dashboard/users/parentalcontrol' }, + { from: 'userpassword.html', to: '/dashboard/users/password' }, + { from: 'userprofiles.html', to: '/dashboard/users' } +]; diff --git a/src/apps/experimental/routes/dashboard/activity.tsx b/src/apps/dashboard/routes/activity.tsx similarity index 97% rename from src/apps/experimental/routes/dashboard/activity.tsx rename to src/apps/dashboard/routes/activity.tsx index f007e104d3..fa3a9135bd 100644 --- a/src/apps/experimental/routes/dashboard/activity.tsx +++ b/src/apps/dashboard/routes/activity.tsx @@ -19,9 +19,9 @@ import { parseISO8601Date, toLocaleDateString, toLocaleTimeString } from 'script import globalize from 'scripts/globalize'; import { toBoolean } from 'utils/string'; -import LogLevelChip from '../../components/activityTable/LogLevelChip'; -import OverviewCell from '../../components/activityTable/OverviewCell'; -import GridActionsCellLink from '../../components/GridActionsCellLink'; +import LogLevelChip from '../components/activityTable/LogLevelChip'; +import OverviewCell from '../components/activityTable/OverviewCell'; +import GridActionsCellLink from '../components/dataGrid/GridActionsCellLink'; const DEFAULT_PAGE_SIZE = 25; const VIEW_PARAM = 'useractivity'; @@ -68,7 +68,7 @@ const Activity = () => { sx={{ padding: 0 }} title={users[row.UserId]?.Name ?? undefined} component={Link} - to={`/useredit.html?userId=${row.UserId}`} + to={`/dashboard/users/profile?userId=${row.UserId}`} > diff --git a/src/apps/stable/routes/dashboard/notifications.tsx b/src/apps/dashboard/routes/notifications.tsx similarity index 89% rename from src/apps/stable/routes/dashboard/notifications.tsx rename to src/apps/dashboard/routes/notifications.tsx index ca874d1333..6f673c753f 100644 --- a/src/apps/stable/routes/dashboard/notifications.tsx +++ b/src/apps/dashboard/routes/notifications.tsx @@ -9,7 +9,7 @@ const PluginLink = () => ( __html: ` ${globalize.translate('GetThePlugin')} ` diff --git a/src/apps/stable/routes/user/userlibraryaccess.tsx b/src/apps/dashboard/routes/users/access.tsx similarity index 100% rename from src/apps/stable/routes/user/userlibraryaccess.tsx rename to src/apps/dashboard/routes/users/access.tsx diff --git a/src/apps/stable/routes/user/usernew.tsx b/src/apps/dashboard/routes/users/add.tsx similarity index 99% rename from src/apps/stable/routes/user/usernew.tsx rename to src/apps/dashboard/routes/users/add.tsx index 22758500ea..116895e947 100644 --- a/src/apps/stable/routes/user/usernew.tsx +++ b/src/apps/dashboard/routes/users/add.tsx @@ -140,7 +140,7 @@ const UserNew: FunctionComponent = () => { } window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () { - Dashboard.navigate('useredit.html?userId=' + user.Id) + Dashboard.navigate('/dashboard/users/profile?userId=' + user.Id) .catch(err => { console.error('[usernew] failed to navigate to edit user page', err); }); diff --git a/src/apps/stable/routes/user/userprofiles.tsx b/src/apps/dashboard/routes/users/index.tsx similarity index 94% rename from src/apps/stable/routes/user/userprofiles.tsx rename to src/apps/dashboard/routes/users/index.tsx index dc8a6e86fd..6789a00ce8 100644 --- a/src/apps/stable/routes/user/userprofiles.tsx +++ b/src/apps/dashboard/routes/users/index.tsx @@ -85,21 +85,21 @@ const UserProfiles: FunctionComponent = () => { callback: function (id: string) { switch (id) { case 'open': - Dashboard.navigate('useredit.html?userId=' + userId) + Dashboard.navigate('/dashboard/users/profile?userId=' + userId) .catch(err => { console.error('[userprofiles] failed to navigate to user edit page', err); }); break; case 'access': - Dashboard.navigate('userlibraryaccess.html?userId=' + userId) + Dashboard.navigate('/dashboard/users/access?userId=' + userId) .catch(err => { console.error('[userprofiles] failed to navigate to user library page', err); }); break; case 'parentalcontrol': - Dashboard.navigate('userparentalcontrol.html?userId=' + userId) + Dashboard.navigate('/dashboard/users/parentalcontrol?userId=' + userId) .catch(err => { console.error('[userprofiles] failed to navigate to parental control page', err); }); @@ -146,7 +146,7 @@ const UserProfiles: FunctionComponent = () => { }); (page.querySelector('#btnAddUser') as HTMLButtonElement).addEventListener('click', function() { - Dashboard.navigate('usernew.html') + Dashboard.navigate('/dashboard/users/add') .catch(err => { console.error('[userprofiles] failed to navigate to new user page', err); }); diff --git a/src/apps/stable/routes/user/userparentalcontrol.tsx b/src/apps/dashboard/routes/users/parentalcontrol.tsx similarity index 100% rename from src/apps/stable/routes/user/userparentalcontrol.tsx rename to src/apps/dashboard/routes/users/parentalcontrol.tsx diff --git a/src/apps/stable/routes/user/userpassword.tsx b/src/apps/dashboard/routes/users/password.tsx similarity index 100% rename from src/apps/stable/routes/user/userpassword.tsx rename to src/apps/dashboard/routes/users/password.tsx diff --git a/src/apps/stable/routes/user/useredit.tsx b/src/apps/dashboard/routes/users/profile.tsx similarity index 99% rename from src/apps/stable/routes/user/useredit.tsx rename to src/apps/dashboard/routes/users/profile.tsx index c4acdfaaee..05d3b72cdf 100644 --- a/src/apps/stable/routes/user/useredit.tsx +++ b/src/apps/dashboard/routes/users/profile.tsx @@ -32,7 +32,7 @@ const getCheckedElementDataIds = (elements: NodeListOf) => ( ); function onSaveComplete() { - Dashboard.navigate('userprofiles.html') + Dashboard.navigate('/dashboard/users') .catch(err => { console.error('[useredit] failed to navigate to user profile', err); }); diff --git a/src/apps/experimental/App.tsx b/src/apps/experimental/App.tsx index 0c0778b74e..b17e9054ee 100644 --- a/src/apps/experimental/App.tsx +++ b/src/apps/experimental/App.tsx @@ -1,14 +1,16 @@ import React from 'react'; import { Navigate, Route, Routes } from 'react-router-dom'; +import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App'; +import { REDIRECTS } from 'apps/stable/routes/_redirects'; import ConnectionRequired from 'components/ConnectionRequired'; -import ServerContentPage from 'components/ServerContentPage'; import { toAsyncPageRoute } from 'components/router/AsyncRoute'; import { toViewManagerPageRoute } from 'components/router/LegacyRoute'; +import { toRedirectRoute } from 'components/router/Redirect'; import AppLayout from './AppLayout'; -import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './routes/asyncRoutes'; -import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes'; +import { ASYNC_USER_ROUTES } from './routes/asyncRoutes'; +import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes'; const ExperimentalApp = () => { return ( @@ -20,26 +22,25 @@ const ExperimentalApp = () => { {LEGACY_USER_ROUTES.map(toViewManagerPageRoute)} - {/* Admin routes */} - }> - {ASYNC_ADMIN_ROUTES.map(toAsyncPageRoute)} - {LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)} - - - } /> - - {/* Public routes */} }> } /> {LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute)} - - {/* Redirects for old paths */} - } /> + + {/* Redirects for old paths */} + {REDIRECTS.map(toRedirectRoute)} + + {/* Ignore dashboard routes */} + {Object.entries(DASHBOARD_APP_PATHS).map(([ key, path ]) => ( + + ))} ); }; diff --git a/src/apps/experimental/AppLayout.tsx b/src/apps/experimental/AppLayout.tsx index 71824e0d73..1f4860637c 100644 --- a/src/apps/experimental/AppLayout.tsx +++ b/src/apps/experimental/AppLayout.tsx @@ -1,18 +1,17 @@ import React, { useCallback, useEffect, useState } from 'react'; import AppBar from '@mui/material/AppBar'; import Box from '@mui/material/Box'; -import { ThemeProvider } from '@mui/material/styles'; +import { useTheme } from '@mui/material/styles'; import { Outlet, useLocation } from 'react-router-dom'; -import AppHeader from 'components/AppHeader'; -import Backdrop from 'components/Backdrop'; +import AppBody from 'components/AppBody'; +import ElevationScroll from 'components/ElevationScroll'; +import { DRAWER_WIDTH } from 'components/ResponsiveDrawer'; import { useApi } from 'hooks/useApi'; import { useLocalStorage } from 'hooks/useLocalStorage'; import AppToolbar from './components/AppToolbar'; -import AppDrawer, { DRAWER_WIDTH, isDrawerPath } from './components/drawers/AppDrawer'; -import ElevationScroll from './components/ElevationScroll'; -import theme from './theme'; +import AppDrawer, { isDrawerPath } from './components/drawers/AppDrawer'; import './AppOverrides.scss'; @@ -29,6 +28,7 @@ const AppLayout = () => { const [ isDrawerActive, setIsDrawerActive ] = useState(appSettings.isDrawerPinned); const { user } = useApi(); const location = useLocation(); + const theme = useTheme(); const isDrawerAvailable = isDrawerPath(location.pathname); const isDrawerOpen = isDrawerActive && isDrawerAvailable && Boolean(user); @@ -47,67 +47,54 @@ const AppLayout = () => { }, [ isDrawerActive, setIsDrawerActive ]); return ( - - - -
- {/* - * TODO: These components are not used, but views interact with them directly so the need to be - * present in the dom. We add them in a hidden element to prevent errors. - */} - -
- - - - muiTheme.zIndex.drawer + 1 }} - > - - - - - - - + + muiTheme.zIndex.drawer + 1 }} > -
-
- -
- + + + + + + + + + + - + ); }; diff --git a/src/apps/experimental/AppOverrides.scss b/src/apps/experimental/AppOverrides.scss index c365a5b295..cece6608c1 100644 --- a/src/apps/experimental/AppOverrides.scss +++ b/src/apps/experimental/AppOverrides.scss @@ -10,11 +10,6 @@ $mui-bp-xl: 1536px; position: relative; } -// Fix dashboard pages layout to work with drawer -.dashboardDocument .skinBody { - position: unset; -} - // Hide some items from the user "settings" page that are in the drawer #myPreferencesMenuPage { .lnkQuickConnectPreferences, @@ -26,8 +21,7 @@ $mui-bp-xl: 1536px; // Fix the padding of some pages .homePage.libraryPage, // Home page -.libraryPage:not(.withTabs), // Tabless library pages -.content-primary.content-primary { // Dashboard pages +.libraryPage:not(.withTabs) { // Tabless library pages padding-top: 3.25rem !important; } diff --git a/src/apps/experimental/components/AppToolbar/index.tsx b/src/apps/experimental/components/AppToolbar/index.tsx index 0317ae92a3..ed447ae694 100644 --- a/src/apps/experimental/components/AppToolbar/index.tsx +++ b/src/apps/experimental/components/AppToolbar/index.tsx @@ -1,22 +1,14 @@ -import ArrowBack from '@mui/icons-material/ArrowBack'; -import MenuIcon from '@mui/icons-material/Menu'; import SearchIcon from '@mui/icons-material/Search'; -import Box from '@mui/material/Box'; import IconButton from '@mui/material/IconButton'; -import Toolbar from '@mui/material/Toolbar'; import Tooltip from '@mui/material/Tooltip'; -import Typography from '@mui/material/Typography'; import React, { FC } from 'react'; import { Link, useLocation } from 'react-router-dom'; -import appIcon from 'assets/img/icon-transparent.png'; -import { appRouter } from 'components/router/appRouter'; -import { useApi } from 'hooks/useApi'; +import AppToolbar from 'components/toolbar/AppToolbar'; import globalize from 'scripts/globalize'; import AppTabs from '../tabs/AppTabs'; import { isDrawerPath } from '../drawers/AppDrawer'; -import UserMenuButton from './UserMenuButton'; import RemotePlayButton from './RemotePlayButton'; import SyncPlayButton from './SyncPlayButton'; @@ -25,120 +17,40 @@ interface AppToolbarProps { onDrawerButtonClick: (event: React.MouseEvent) => void } -const onBackButtonClick = () => { - appRouter.back() - .catch(err => { - console.error('[AppToolbar] error calling appRouter.back', err); - }); -}; - -const AppToolbar: FC = ({ +const ExperimentalAppToolbar: FC = ({ isDrawerOpen, onDrawerButtonClick }) => { - const { user } = useApi(); - const isUserLoggedIn = Boolean(user); const location = useLocation(); - const isDrawerAvailable = isDrawerPath(location.pathname); - const isBackButtonAvailable = appRouter.canGoBack(); return ( - - {isUserLoggedIn && isDrawerAvailable && ( - - - - - - )} - - {isBackButtonAvailable && ( - - - - - - )} - - - - - Jellyfin - - - - - - {isUserLoggedIn && ( + - - - + + - - - - - - - - - - + + + + + - )} - + } + isDrawerAvailable={isDrawerAvailable} + isDrawerOpen={isDrawerOpen} + onDrawerButtonClick={onDrawerButtonClick} + > + + ); }; -export default AppToolbar; +export default ExperimentalAppToolbar; diff --git a/src/apps/experimental/components/AppToolbar/menus/SyncPlayMenu.tsx b/src/apps/experimental/components/AppToolbar/menus/SyncPlayMenu.tsx index cb377784e8..6e08b3fea6 100644 --- a/src/apps/experimental/components/AppToolbar/menus/SyncPlayMenu.tsx +++ b/src/apps/experimental/components/AppToolbar/menus/SyncPlayMenu.tsx @@ -19,6 +19,7 @@ import React, { FC, useCallback, useEffect, useState } from 'react'; import { pluginManager } from 'components/pluginManager'; import { useApi } from 'hooks/useApi'; +import { useSyncPlayGroups } from 'hooks/useSyncPlayGroups'; import globalize from 'scripts/globalize'; import { PluginType } from 'types/plugin'; import Events from 'utils/events'; @@ -47,7 +48,6 @@ const SyncPlayMenu: FC = ({ }) => { const [ syncPlay, setSyncPlay ] = useState(); const { __legacyApiClient__, api, user } = useApi(); - const [ groups, setGroups ] = useState([]); const [ currentGroup, setCurrentGroup ] = useState(); const isSyncPlayEnabled = Boolean(currentGroup); @@ -55,18 +55,7 @@ const SyncPlayMenu: FC = ({ setSyncPlay(pluginManager.firstOfType(PluginType.SyncPlay)?.instance); }, []); - useEffect(() => { - const fetchGroups = async () => { - if (api) { - setGroups((await getSyncPlayApi(api).syncPlayGetGroups()).data); - } - }; - - fetchGroups() - .catch(err => { - console.error('[SyncPlayMenu] unable to fetch SyncPlay groups', err); - }); - }, [ api ]); + const { data: groups } = useSyncPlayGroups(); const onGroupAddClick = useCallback(() => { if (api && user) { @@ -224,7 +213,7 @@ const SyncPlayMenu: FC = ({ /> ); - } else if (groups.length === 0 && user?.Policy?.SyncPlayAccess !== SyncPlayUserAccessType.CreateAndJoinGroups) { + } else if (!groups?.length && user?.Policy?.SyncPlayAccess !== SyncPlayUserAccessType.CreateAndJoinGroups) { menuItems.push( @@ -234,7 +223,7 @@ const SyncPlayMenu: FC = ({ ); } else { - if (groups.length > 0) { + if (groups && groups.length > 0) { groups.forEach(group => { menuItems.push( !DRAWERLESS_ROUTES.includes(route.path)); -const ADMIN_DRAWER_ROUTES = [ - ...ASYNC_ADMIN_ROUTES, - ...LEGACY_ADMIN_ROUTES, - { path: '/configurationpage' } // Plugin configuration page -].filter(route => !DRAWERLESS_ROUTES.includes(route.path)); - /** Utility function to check if a path has a drawer. */ export const isDrawerPath = (path: string) => ( MAIN_DRAWER_ROUTES.some(route => route.path === path || `/${route.path}` === path) - || ADMIN_DRAWER_ROUTES.some(route => route.path === path || `/${route.path}` === path) ); const AppDrawer: FC = ({ open = false, onClose, onOpen -}) => ( - - { - MAIN_DRAWER_ROUTES.map(route => ( - - - - } - /> - )) - } - { - ADMIN_DRAWER_ROUTES.map(route => ( - - - - - - - - } - /> - )) - } - -); +}) => { + const location = useLocation(); + const hasSecondaryToolBar = isTabPath(location.pathname); + + return ( + + + + ); +}; export default AppDrawer; diff --git a/src/apps/experimental/components/drawers/MainDrawerContent.tsx b/src/apps/experimental/components/drawers/MainDrawerContent.tsx index f402189cf7..351076a025 100644 --- a/src/apps/experimental/components/drawers/MainDrawerContent.tsx +++ b/src/apps/experimental/components/drawers/MainDrawerContent.tsx @@ -17,12 +17,12 @@ import ListSubheader from '@mui/material/ListSubheader'; import React, { useEffect, useState } from 'react'; import { useLocation } from 'react-router-dom'; +import ListItemLink from 'components/ListItemLink'; +import { appRouter } from 'components/router/appRouter'; import { useApi } from 'hooks/useApi'; import { useWebConfig } from 'hooks/useWebConfig'; import globalize from 'scripts/globalize'; -import { appRouter } from 'components/router/appRouter'; -import ListItemLink from './ListItemLink'; import LibraryIcon from '../LibraryIcon'; const MainDrawerContent = () => { @@ -150,7 +150,7 @@ const MainDrawerContent = () => { } > - + @@ -158,7 +158,7 @@ const MainDrawerContent = () => { - + diff --git a/src/apps/experimental/components/library/AlphabetPicker.tsx b/src/apps/experimental/components/library/AlphabetPicker.tsx new file mode 100644 index 0000000000..aec6d2eb9b --- /dev/null +++ b/src/apps/experimental/components/library/AlphabetPicker.tsx @@ -0,0 +1,85 @@ +import React, { useCallback } from 'react'; +import classNames from 'classnames'; + +import Box from '@mui/material/Box'; +import ToggleButton from '@mui/material/ToggleButton'; +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; + +import { LibraryViewSettings } from 'types/library'; +import 'components/alphaPicker/style.scss'; + +interface AlphabetPickerProps { + className?: string; + libraryViewSettings: LibraryViewSettings; + setLibraryViewSettings: React.Dispatch< + React.SetStateAction + >; +} + +const AlphabetPicker: React.FC = ({ + className, + libraryViewSettings, + setLibraryViewSettings +}) => { + const handleValue = useCallback( + ( + event: React.MouseEvent, + newValue: string | null | undefined + ) => { + setLibraryViewSettings((prevState) => ({ + ...prevState, + StartIndex: 0, + Alphabet: newValue + })); + }, + [setLibraryViewSettings] + ); + + const containerClassName = classNames( + 'alphaPicker', + className, + 'alphaPicker-fixed-right' + ); + + const btnClassName = classNames( + 'paper-icon-button-light', + 'alphaPickerButton' + ); + + const letters = ['#', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']; + + return ( + + + {letters.map((l) => ( + + {l} + + ))} + + + ); +}; + +export default AlphabetPicker; diff --git a/src/apps/experimental/components/library/NewCollectionButton.tsx b/src/apps/experimental/components/library/NewCollectionButton.tsx new file mode 100644 index 0000000000..e337de7ddd --- /dev/null +++ b/src/apps/experimental/components/library/NewCollectionButton.tsx @@ -0,0 +1,34 @@ +import React, { FC, useCallback } from 'react'; +import { IconButton } from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import globalize from 'scripts/globalize'; + +const NewCollectionButton: FC = () => { + const showCollectionEditor = useCallback(() => { + import('components/collectionEditor/collectionEditor').then( + ({ default: CollectionEditor }) => { + const serverId = window.ApiClient.serverId(); + const collectionEditor = new CollectionEditor(); + collectionEditor.show({ + items: [], + serverId: serverId + }).catch(() => { + // closed collection editor + }); + }).catch(err => { + console.error('[NewCollection] failed to load collection editor', err); + }); + }, []); + + return ( + + + + ); +}; + +export default NewCollectionButton; diff --git a/src/apps/experimental/components/library/Pagination.tsx b/src/apps/experimental/components/library/Pagination.tsx new file mode 100644 index 0000000000..3d1026254f --- /dev/null +++ b/src/apps/experimental/components/library/Pagination.tsx @@ -0,0 +1,91 @@ +import React, { FC, useCallback } from 'react'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; +import Box from '@mui/material/Box'; +import ButtonGroup from '@mui/material/ButtonGroup'; +import IconButton from '@mui/material/IconButton'; + +import globalize from 'scripts/globalize'; +import * as userSettings from 'scripts/settings/userSettings'; +import { LibraryViewSettings } from 'types/library'; + +interface PaginationProps { + libraryViewSettings: LibraryViewSettings; + setLibraryViewSettings: React.Dispatch>; + totalRecordCount: number; +} + +const Pagination: FC = ({ + libraryViewSettings, + setLibraryViewSettings, + totalRecordCount +}) => { + const limit = userSettings.libraryPageSize(undefined); + const startIndex = libraryViewSettings.StartIndex || 0; + const recordsStart = totalRecordCount ? startIndex + 1 : 0; + const recordsEnd = limit ? + Math.min(startIndex + limit, totalRecordCount) : + totalRecordCount; + const showControls = limit > 0 && limit < totalRecordCount; + + const onNextPageClick = useCallback(() => { + if (limit > 0) { + const newIndex = startIndex + limit; + setLibraryViewSettings((prevState) => ({ + ...prevState, + StartIndex: newIndex + })); + } + }, [limit, setLibraryViewSettings, startIndex]); + + const onPreviousPageClick = useCallback(() => { + if (limit > 0) { + const newIndex = Math.max(0, startIndex - limit); + setLibraryViewSettings((prevState) => ({ + ...prevState, + StartIndex: newIndex + })); + } + }, [limit, setLibraryViewSettings, startIndex]); + + return ( + + + + {globalize.translate( + 'ListPaging', + recordsStart, + recordsEnd, + totalRecordCount + )} + + {showControls && ( + + + + + + = totalRecordCount } + onClick={onNextPageClick} + > + + + + )} + + + ); +}; + +export default Pagination; diff --git a/src/apps/experimental/components/library/PlayAllButton.tsx b/src/apps/experimental/components/library/PlayAllButton.tsx new file mode 100644 index 0000000000..d7fb090380 --- /dev/null +++ b/src/apps/experimental/components/library/PlayAllButton.tsx @@ -0,0 +1,57 @@ +import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'; +import React, { FC, useCallback } from 'react'; +import { IconButton } from '@mui/material'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; + +import { playbackManager } from 'components/playback/playbackmanager'; +import globalize from 'scripts/globalize'; +import { getFiltersQuery } from 'utils/items'; +import { LibraryViewSettings } from 'types/library'; +import { LibraryTab } from 'types/libraryTab'; + +interface PlayAllButtonProps { + item: BaseItemDto | undefined; + items: BaseItemDto[]; + viewType: LibraryTab; + hasFilters: boolean; + libraryViewSettings: LibraryViewSettings +} + +const PlayAllButton: FC = ({ item, items, viewType, hasFilters, libraryViewSettings }) => { + const play = useCallback(() => { + if (item && !hasFilters) { + playbackManager.play({ + items: [item], + autoplay: true, + queryOptions: { + SortBy: [libraryViewSettings.SortBy], + SortOrder: [libraryViewSettings.SortOrder] + } + }); + } else { + playbackManager.play({ + items: items, + autoplay: true, + queryOptions: { + ParentId: item?.Id ?? undefined, + ...getFiltersQuery(viewType, libraryViewSettings), + SortBy: [libraryViewSettings.SortBy], + SortOrder: [libraryViewSettings.SortOrder] + } + + }); + } + }, [hasFilters, item, items, libraryViewSettings, viewType]); + + return ( + + + + ); +}; + +export default PlayAllButton; diff --git a/src/apps/experimental/components/library/QueueButton.tsx b/src/apps/experimental/components/library/QueueButton.tsx new file mode 100644 index 0000000000..fdc6a7666b --- /dev/null +++ b/src/apps/experimental/components/library/QueueButton.tsx @@ -0,0 +1,39 @@ +import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'; +import React, { FC, useCallback } from 'react'; +import { IconButton } from '@mui/material'; +import QueueIcon from '@mui/icons-material/Queue'; + +import { playbackManager } from 'components/playback/playbackmanager'; +import globalize from 'scripts/globalize'; + +interface QueueButtonProps { + item: BaseItemDto | undefined + items: BaseItemDto[]; + hasFilters: boolean; +} + +const QueueButton: FC = ({ item, items, hasFilters }) => { + const queue = useCallback(() => { + if (item && !hasFilters) { + playbackManager.queue({ + items: [item] + }); + } else { + playbackManager.queue({ + items: items + }); + } + }, [hasFilters, item, items]); + + return ( + + + + ); +}; + +export default QueueButton; diff --git a/src/apps/experimental/components/library/ShuffleButton.tsx b/src/apps/experimental/components/library/ShuffleButton.tsx new file mode 100644 index 0000000000..c81ee4c4ba --- /dev/null +++ b/src/apps/experimental/components/library/ShuffleButton.tsx @@ -0,0 +1,49 @@ +import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'; +import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by'; +import React, { FC, useCallback } from 'react'; +import { IconButton } from '@mui/material'; +import ShuffleIcon from '@mui/icons-material/Shuffle'; + +import { playbackManager } from 'components/playback/playbackmanager'; +import globalize from 'scripts/globalize'; +import { getFiltersQuery } from 'utils/items'; +import { LibraryViewSettings } from 'types/library'; +import { LibraryTab } from 'types/libraryTab'; + +interface ShuffleButtonProps { + item: BaseItemDto | undefined; + items: BaseItemDto[]; + viewType: LibraryTab + hasFilters: boolean; + libraryViewSettings: LibraryViewSettings +} + +const ShuffleButton: FC = ({ item, items, viewType, hasFilters, libraryViewSettings }) => { + const shuffle = useCallback(() => { + if (item && !hasFilters) { + playbackManager.shuffle(item); + } else { + playbackManager.play({ + items: items, + autoplay: true, + queryOptions: { + ParentId: item?.Id ?? undefined, + ...getFiltersQuery(viewType, libraryViewSettings), + SortBy: [ItemSortBy.Random] + } + }); + } + }, [hasFilters, item, items, libraryViewSettings, viewType]); + + return ( + + + + ); +}; + +export default ShuffleButton; diff --git a/src/apps/experimental/components/library/SortButton.tsx b/src/apps/experimental/components/library/SortButton.tsx index 7deeae349b..2c7425f0de 100644 --- a/src/apps/experimental/components/library/SortButton.tsx +++ b/src/apps/experimental/components/library/SortButton.tsx @@ -98,7 +98,7 @@ const SortButton: FC = ({ title={globalize.translate('Sort')} sx={{ ml: 2 }} aria-describedby={id} - className='paper-icon-button-light btnShuffle autoSize' + className='paper-icon-button-light btnSort autoSize' onClick={handleClick} > diff --git a/src/apps/experimental/components/library/ViewSettingsButton.tsx b/src/apps/experimental/components/library/ViewSettingsButton.tsx index cec5090acc..b1ca1679e0 100644 --- a/src/apps/experimental/components/library/ViewSettingsButton.tsx +++ b/src/apps/experimental/components/library/ViewSettingsButton.tsx @@ -100,7 +100,7 @@ const ViewSettingsButton: FC = ({ title={globalize.translate('ButtonSelectView')} sx={{ ml: 2 }} aria-describedby={id} - className='paper-icon-button-light btnShuffle autoSize' + className='paper-icon-button-light btnSelectView autoSize' onClick={handleClick} > diff --git a/src/apps/experimental/routes/asyncRoutes/admin.ts b/src/apps/experimental/routes/asyncRoutes/admin.ts deleted file mode 100644 index 7e8c0eca16..0000000000 --- a/src/apps/experimental/routes/asyncRoutes/admin.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { AsyncRoute, AsyncRouteType } from 'components/router/AsyncRoute'; - -export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [ - { path: 'dashboard/activity', page: 'dashboard/activity', type: AsyncRouteType.Experimental }, - { path: 'notificationsettings.html', page: 'dashboard/notifications' }, - { path: 'usernew.html', page: 'user/usernew' }, - { path: 'userprofiles.html', page: 'user/userprofiles' }, - { path: 'useredit.html', page: 'user/useredit' }, - { path: 'userlibraryaccess.html', page: 'user/userlibraryaccess' }, - { path: 'userparentalcontrol.html', page: 'user/userparentalcontrol' }, - { path: 'userpassword.html', page: 'user/userpassword' } -]; diff --git a/src/apps/experimental/routes/asyncRoutes/index.ts b/src/apps/experimental/routes/asyncRoutes/index.ts index 9dd4fb3c99..e5abc85650 100644 --- a/src/apps/experimental/routes/asyncRoutes/index.ts +++ b/src/apps/experimental/routes/asyncRoutes/index.ts @@ -1,2 +1 @@ -export * from './admin'; export * from './user'; diff --git a/src/apps/experimental/routes/asyncRoutes/user.ts b/src/apps/experimental/routes/asyncRoutes/user.ts index d7ea0dd7d4..023e292edb 100644 --- a/src/apps/experimental/routes/asyncRoutes/user.ts +++ b/src/apps/experimental/routes/asyncRoutes/user.ts @@ -1,6 +1,7 @@ import { AsyncRoute, AsyncRouteType } from '../../../../components/router/AsyncRoute'; export const ASYNC_USER_ROUTES: AsyncRoute[] = [ + { path: 'quickconnect', page: 'quickConnect' }, { path: 'search.html', page: 'search' }, { path: 'userprofile.html', page: 'user/userprofile' }, { path: 'home.html', page: 'home', type: AsyncRouteType.Experimental }, diff --git a/src/apps/experimental/routes/legacyRoutes/index.ts b/src/apps/experimental/routes/legacyRoutes/index.ts index 2931c568e8..bc46c94c54 100644 --- a/src/apps/experimental/routes/legacyRoutes/index.ts +++ b/src/apps/experimental/routes/legacyRoutes/index.ts @@ -1,3 +1,2 @@ -export * from './admin'; export * from './public'; export * from './user'; diff --git a/src/apps/experimental/routes/legacyRoutes/user.ts b/src/apps/experimental/routes/legacyRoutes/user.ts index 49afc715f1..965767d915 100644 --- a/src/apps/experimental/routes/legacyRoutes/user.ts +++ b/src/apps/experimental/routes/legacyRoutes/user.ts @@ -49,12 +49,6 @@ export const LEGACY_USER_ROUTES: LegacyRoute[] = [ controller: 'user/home/index', view: 'user/home/index.html' } - }, { - path: 'mypreferencesquickconnect.html', - pageProps: { - controller: 'user/quickConnect/index', - view: 'user/quickConnect/index.html' - } }, { path: 'mypreferencesplayback.html', pageProps: { diff --git a/src/apps/stable/App.tsx b/src/apps/stable/App.tsx index 73c73f5881..9b0adbab9c 100644 --- a/src/apps/stable/App.tsx +++ b/src/apps/stable/App.tsx @@ -1,26 +1,21 @@ import React from 'react'; import { Navigate, Outlet, Route, Routes } from 'react-router-dom'; -import AppHeader from 'components/AppHeader'; -import Backdrop from 'components/Backdrop'; -import ServerContentPage from 'components/ServerContentPage'; +import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App'; +import AppBody from 'components/AppBody'; import ConnectionRequired from 'components/ConnectionRequired'; import { toAsyncPageRoute } from 'components/router/AsyncRoute'; import { toViewManagerPageRoute } from 'components/router/LegacyRoute'; +import { toRedirectRoute } from 'components/router/Redirect'; -import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './routes/asyncRoutes'; -import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes'; +import { ASYNC_USER_ROUTES } from './routes/asyncRoutes'; +import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes'; +import { REDIRECTS } from './routes/_redirects'; const Layout = () => ( - <> - - - -
-
- -
- + + + ); const StableApp = () => ( @@ -32,16 +27,6 @@ const StableApp = () => ( {LEGACY_USER_ROUTES.map(toViewManagerPageRoute)} - {/* Admin routes */} - }> - {ASYNC_ADMIN_ROUTES.map(toAsyncPageRoute)} - {LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)} - - - } /> - - {/* Public routes */} }> } /> @@ -51,10 +36,19 @@ const StableApp = () => ( {/* Suppress warnings for unhandled routes */} - - {/* Redirects for old paths */} - } /> + + {/* Redirects for old paths */} + {REDIRECTS.map(toRedirectRoute)} + + {/* Ignore dashboard routes */} + {Object.entries(DASHBOARD_APP_PATHS).map(([ key, path ]) => ( + + ))} ); diff --git a/src/apps/stable/routes/_redirects.ts b/src/apps/stable/routes/_redirects.ts new file mode 100644 index 0000000000..d48c48d995 --- /dev/null +++ b/src/apps/stable/routes/_redirects.ts @@ -0,0 +1,5 @@ +import type { Redirect } from 'components/router/Redirect'; + +export const REDIRECTS: Redirect[] = [ + { from: 'mypreferencesquickconnect.html', to: '/quickconnect' } +]; diff --git a/src/apps/stable/routes/asyncRoutes/admin.ts b/src/apps/stable/routes/asyncRoutes/admin.ts deleted file mode 100644 index 72bcc6f32b..0000000000 --- a/src/apps/stable/routes/asyncRoutes/admin.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { AsyncRoute } from '../../../../components/router/AsyncRoute'; - -export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [ - { path: 'notificationsettings.html', page: 'dashboard/notifications' }, - { path: 'usernew.html', page: 'user/usernew' }, - { path: 'userprofiles.html', page: 'user/userprofiles' }, - { path: 'useredit.html', page: 'user/useredit' }, - { path: 'userlibraryaccess.html', page: 'user/userlibraryaccess' }, - { path: 'userparentalcontrol.html', page: 'user/userparentalcontrol' }, - { path: 'userpassword.html', page: 'user/userpassword' } -]; diff --git a/src/apps/stable/routes/asyncRoutes/index.ts b/src/apps/stable/routes/asyncRoutes/index.ts index 9dd4fb3c99..e5abc85650 100644 --- a/src/apps/stable/routes/asyncRoutes/index.ts +++ b/src/apps/stable/routes/asyncRoutes/index.ts @@ -1,2 +1 @@ -export * from './admin'; export * from './user'; diff --git a/src/apps/stable/routes/asyncRoutes/user.ts b/src/apps/stable/routes/asyncRoutes/user.ts index 6a683323bc..153e310b35 100644 --- a/src/apps/stable/routes/asyncRoutes/user.ts +++ b/src/apps/stable/routes/asyncRoutes/user.ts @@ -1,6 +1,7 @@ import { AsyncRoute } from '../../../../components/router/AsyncRoute'; export const ASYNC_USER_ROUTES: AsyncRoute[] = [ + { path: 'quickconnect', page: 'quickConnect' }, { path: 'search.html', page: 'search' }, { path: 'userprofile.html', page: 'user/userprofile' } ]; diff --git a/src/apps/stable/routes/legacyRoutes/admin.ts b/src/apps/stable/routes/legacyRoutes/admin.ts deleted file mode 100644 index 92aaef8041..0000000000 --- a/src/apps/stable/routes/legacyRoutes/admin.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { LegacyRoute } from '../../../../components/router/LegacyRoute'; - -export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [ - { - path: 'dashboard.html', - pageProps: { - controller: 'dashboard/dashboard', - view: 'dashboard/dashboard.html' - } - }, { - path: 'dashboardgeneral.html', - pageProps: { - controller: 'dashboard/general', - view: 'dashboard/general.html' - } - }, { - path: 'networking.html', - pageProps: { - controller: 'dashboard/networking', - view: 'dashboard/networking.html' - } - }, { - path: 'devices.html', - pageProps: { - controller: 'dashboard/devices/devices', - view: 'dashboard/devices/devices.html' - } - }, { - path: 'device.html', - pageProps: { - controller: 'dashboard/devices/device', - view: 'dashboard/devices/device.html' - } - }, { - path: 'quickConnect.html', - pageProps: { - controller: 'dashboard/quickConnect', - view: 'dashboard/quickConnect.html' - } - }, { - path: 'dlnaprofile.html', - pageProps: { - controller: 'dashboard/dlna/profile', - view: 'dashboard/dlna/profile.html' - } - }, { - path: 'dlnaprofiles.html', - pageProps: { - controller: 'dashboard/dlna/profiles', - view: 'dashboard/dlna/profiles.html' - } - }, { - path: 'dlnasettings.html', - pageProps: { - controller: 'dashboard/dlna/settings', - view: 'dashboard/dlna/settings.html' - } - }, { - path: 'addplugin.html', - pageProps: { - controller: 'dashboard/plugins/add/index', - view: 'dashboard/plugins/add/index.html' - } - }, { - path: 'library.html', - pageProps: { - controller: 'dashboard/library', - view: 'dashboard/library.html' - } - }, { - path: 'librarydisplay.html', - pageProps: { - controller: 'dashboard/librarydisplay', - view: 'dashboard/librarydisplay.html' - } - }, { - path: 'edititemmetadata.html', - pageProps: { - controller: 'edititemmetadata', - view: 'edititemmetadata.html' - } - }, { - path: 'encodingsettings.html', - pageProps: { - controller: 'dashboard/encodingsettings', - view: 'dashboard/encodingsettings.html' - } - }, { - path: 'log.html', - pageProps: { - controller: 'dashboard/logs', - view: 'dashboard/logs.html' - } - }, { - path: 'metadataimages.html', - pageProps: { - controller: 'dashboard/metadataImages', - view: 'dashboard/metadataimages.html' - } - }, { - path: 'metadatanfo.html', - pageProps: { - controller: 'dashboard/metadatanfo', - view: 'dashboard/metadatanfo.html' - } - }, { - path: 'playbackconfiguration.html', - pageProps: { - controller: 'dashboard/playback', - view: 'dashboard/playback.html' - } - }, { - path: 'availableplugins.html', - pageProps: { - controller: 'dashboard/plugins/available/index', - view: 'dashboard/plugins/available/index.html' - } - }, { - path: 'repositories.html', - pageProps: { - controller: 'dashboard/plugins/repositories/index', - view: 'dashboard/plugins/repositories/index.html' - } - }, { - path: 'livetvguideprovider.html', - pageProps: { - controller: 'livetvguideprovider', - view: 'livetvguideprovider.html' - } - }, { - path: 'livetvsettings.html', - pageProps: { - controller: 'livetvsettings', - view: 'livetvsettings.html' - } - }, { - path: 'livetvstatus.html', - pageProps: { - controller: 'livetvstatus', - view: 'livetvstatus.html' - } - }, { - path: 'livetvtuner.html', - pageProps: { - controller: 'livetvtuner', - view: 'livetvtuner.html' - } - }, { - path: 'installedplugins.html', - pageProps: { - controller: 'dashboard/plugins/installed/index', - view: 'dashboard/plugins/installed/index.html' - } - }, { - path: 'scheduledtask.html', - pageProps: { - controller: 'dashboard/scheduledtasks/scheduledtask', - view: 'dashboard/scheduledtasks/scheduledtask.html' - } - }, { - path: 'scheduledtasks.html', - pageProps: { - controller: 'dashboard/scheduledtasks/scheduledtasks', - view: 'dashboard/scheduledtasks/scheduledtasks.html' - } - }, { - path: 'dashboard/activity', - pageProps: { - controller: 'dashboard/serveractivity', - view: 'dashboard/serveractivity.html' - } - }, { - path: 'apikeys.html', - pageProps: { - controller: 'dashboard/apikeys', - view: 'dashboard/apikeys.html' - } - }, { - path: 'streamingsettings.html', - pageProps: { - view: 'dashboard/streaming.html', - controller: 'dashboard/streaming' - } - } -]; diff --git a/src/apps/stable/routes/legacyRoutes/index.ts b/src/apps/stable/routes/legacyRoutes/index.ts index 2931c568e8..bc46c94c54 100644 --- a/src/apps/stable/routes/legacyRoutes/index.ts +++ b/src/apps/stable/routes/legacyRoutes/index.ts @@ -1,3 +1,2 @@ -export * from './admin'; export * from './public'; export * from './user'; diff --git a/src/apps/stable/routes/legacyRoutes/user.ts b/src/apps/stable/routes/legacyRoutes/user.ts index f87bd4383f..fa1fa43ad2 100644 --- a/src/apps/stable/routes/legacyRoutes/user.ts +++ b/src/apps/stable/routes/legacyRoutes/user.ts @@ -49,12 +49,6 @@ export const LEGACY_USER_ROUTES: LegacyRoute[] = [ controller: 'user/home/index', view: 'user/home/index.html' } - }, { - path: 'mypreferencesquickconnect.html', - pageProps: { - controller: 'user/quickConnect/index', - view: 'user/quickConnect/index.html' - } }, { path: 'mypreferencesplayback.html', pageProps: { diff --git a/src/apps/stable/routes/quickConnect/index.tsx b/src/apps/stable/routes/quickConnect/index.tsx new file mode 100644 index 0000000000..8bff06c7ae --- /dev/null +++ b/src/apps/stable/routes/quickConnect/index.tsx @@ -0,0 +1,116 @@ +import { getQuickConnectApi } from '@jellyfin/sdk/lib/utils/api/quick-connect-api'; +import React, { FC, FormEvent, useCallback, useMemo, useState } from 'react'; +import { Link, useSearchParams } from 'react-router-dom'; + +import Page from 'components/Page'; +import globalize from 'scripts/globalize'; +import InputElement from 'elements/InputElement'; +import ButtonElement from 'elements/ButtonElement'; +import { useApi } from 'hooks/useApi'; + +import './quickConnect.scss'; + +const QuickConnectPage: FC = () => { + const { api, user } = useApi(); + const [ searchParams ] = useSearchParams(); + // eslint-disable-next-line react-hooks/exhaustive-deps + const initialValue = useMemo(() => searchParams.get('code') ?? '', []); + const [ code, setCode ] = useState(initialValue); + const [ error, setError ] = useState(); + const [ success, setSuccess ] = useState(false); + + const onCodeChange = useCallback((value: string) => { + setCode(value); + }, []); + + const onSubmitCode = useCallback((e: FormEvent) => { + e.preventDefault(); + setError(undefined); + + const form = e.currentTarget; + if (!form.checkValidity()) { + setError('QuickConnectInvalidCode'); + return; + } + + if (!api) { + console.error('[QuickConnect] cannot authorize, missing api instance'); + setError('UnknownError'); + return; + } + + const userId = searchParams.get('userId') ?? user?.Id; + const normalizedCode = code.replace(/\s/g, ''); + console.log('[QuickConnect] authorizing code %s as user %s', normalizedCode, userId); + + getQuickConnectApi(api) + .authorizeQuickConnect({ + code: normalizedCode, + userId + }) + .then(() => { + setSuccess(true); + }) + .catch(() => { + setError('QuickConnectAuthorizeFail'); + }); + }, [api, code, searchParams, user?.Id]); + + return ( + +
+
+
+

+ {globalize.translate('QuickConnect')} +

+
+ {globalize.translate('QuickConnectDescription')} +
+
+ + {error && ( +
+ {globalize.translate(error)} +
+ )} + + {success ? ( +
+

+ {globalize.translate('QuickConnectAuthorizeSuccess')} +

+ + {globalize.translate('GoHome')} + +
+ ) : ( + <> + + + + )} +
+
+
+
+ ); +}; + +export default QuickConnectPage; diff --git a/src/apps/stable/routes/quickConnect/quickConnect.scss b/src/apps/stable/routes/quickConnect/quickConnect.scss new file mode 100644 index 0000000000..d81e715613 --- /dev/null +++ b/src/apps/stable/routes/quickConnect/quickConnect.scss @@ -0,0 +1,7 @@ +.quickConnectError { + border-radius: 0.2em; + background-color: #160b0b; + color: #f4c7c3; + padding: 0.7em 0.5em; + margin-bottom: 1em; +} diff --git a/src/components/AppBody.tsx b/src/components/AppBody.tsx new file mode 100644 index 0000000000..5d10bca2c9 --- /dev/null +++ b/src/components/AppBody.tsx @@ -0,0 +1,24 @@ +import React, { FC, useEffect } from 'react'; +import viewContainer from './viewContainer'; + +/** + * A simple component that includes the correct structure for ViewManager pages + * to exist alongside standard React pages. + */ +const AppBody: FC = ({ children }) => { + useEffect(() => () => { + // Reset view container state on unload + viewContainer.reset(); + }, []); + + return ( + <> +
+
+ {children} +
+ + ); +}; + +export default AppBody; diff --git a/src/components/AppHeader.tsx b/src/components/AppHeader.tsx index 941d36a940..1ce6a6a8c4 100644 --- a/src/components/AppHeader.tsx +++ b/src/components/AppHeader.tsx @@ -1,19 +1,29 @@ -import React, { useEffect } from 'react'; +import React, { FC, useEffect } from 'react'; -const AppHeader = () => { +interface AppHeaderParams { + isHidden?: boolean +} + +const AppHeader: FC = ({ + isHidden = false +}) => { useEffect(() => { // Initialize the UI components after first render import('../scripts/libraryMenu'); }, []); return ( - <> + /** + * NOTE: These components are not used with the new layouts, but legacy views interact with the elements + * directly so they need to be present in the DOM. We use display: none to hide them and prevent errors. + */ +
- +
); }; diff --git a/src/apps/experimental/components/ElevationScroll.tsx b/src/components/ElevationScroll.tsx similarity index 100% rename from src/apps/experimental/components/ElevationScroll.tsx rename to src/components/ElevationScroll.tsx diff --git a/src/apps/experimental/components/drawers/ListItemLink.tsx b/src/components/ListItemLink.tsx similarity index 100% rename from src/apps/experimental/components/drawers/ListItemLink.tsx rename to src/components/ListItemLink.tsx diff --git a/src/apps/experimental/components/drawers/ResponsiveDrawer.tsx b/src/components/ResponsiveDrawer.tsx similarity index 87% rename from src/apps/experimental/components/drawers/ResponsiveDrawer.tsx rename to src/components/ResponsiveDrawer.tsx index 1f7a554cf4..bc463cca2f 100644 --- a/src/apps/experimental/components/drawers/ResponsiveDrawer.tsx +++ b/src/components/ResponsiveDrawer.tsx @@ -5,14 +5,13 @@ import SwipeableDrawer from '@mui/material/SwipeableDrawer'; import Toolbar from '@mui/material/Toolbar'; import useMediaQuery from '@mui/material/useMediaQuery'; import React, { FC, useCallback } from 'react'; -import { useLocation } from 'react-router-dom'; import browser from 'scripts/browser'; -import { DRAWER_WIDTH } from './AppDrawer'; -import { isTabPath } from '../tabs/tabRoutes'; +export const DRAWER_WIDTH = 240; export interface ResponsiveDrawerProps { + hasSecondaryToolBar?: boolean open: boolean onClose: () => void onOpen: () => void @@ -20,18 +19,17 @@ export interface ResponsiveDrawerProps { const ResponsiveDrawer: FC = ({ children, + hasSecondaryToolBar = false, open = false, onClose, onOpen }) => { - const location = useLocation(); const isSmallScreen = useMediaQuery((theme: Theme) => theme.breakpoints.up('sm')); const isLargeScreen = useMediaQuery((theme: Theme) => theme.breakpoints.up('lg')); - const isTallToolbar = isTabPath(location.pathname) && !isLargeScreen; const getToolbarStyles = useCallback((theme: Theme) => ({ - marginBottom: isTallToolbar ? theme.spacing(6) : 0 - }), [ isTallToolbar ]); + marginBottom: (hasSecondaryToolBar && !isLargeScreen) ? theme.spacing(6) : 0 + }), [ hasSecondaryToolBar, isLargeScreen ]); return ( isSmallScreen ? ( /* DESKTOP DRAWER */ diff --git a/src/components/cardbuilder/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js index 96d7edb06a..d52713283b 100644 --- a/src/components/cardbuilder/cardBuilder.js +++ b/src/components/cardbuilder/cardBuilder.js @@ -5,24 +5,30 @@ */ import escapeHtml from 'escape-html'; -import datetime from '../../scripts/datetime'; -import imageLoader from '../images/imageLoader'; -import itemHelper from '../itemHelper'; + +import cardBuilderUtils from './cardBuilderUtils'; +import browser from 'scripts/browser'; +import datetime from 'scripts/datetime'; +import dom from 'scripts/dom'; +import globalize from 'scripts/globalize'; +import imageHelper from 'scripts/imagehelper'; +import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card'; +import { randomInt } from 'utils/number'; + import focusManager from '../focusManager'; +import imageLoader from '../images/imageLoader'; import indicators from '../indicators/indicators'; -import globalize from '../../scripts/globalize'; +import itemHelper from '../itemHelper'; import layoutManager from '../layoutManager'; -import dom from '../../scripts/dom'; -import browser from '../../scripts/browser'; import { playbackManager } from '../playback/playbackmanager'; -import itemShortcuts from '../shortcuts'; -import imageHelper from '../../scripts/imagehelper'; -import { randomInt } from '../../utils/number.ts'; -import './card.scss'; -import '../../elements/emby-button/paper-icon-button-light'; -import '../guide/programs.scss'; -import ServerConnections from '../ServerConnections'; import { appRouter } from '../router/appRouter'; +import ServerConnections from '../ServerConnections'; +import itemShortcuts from '../shortcuts'; + +import 'elements/emby-button/paper-icon-button-light'; + +import './card.scss'; +import '../guide/programs.scss'; const enableFocusTransform = !browser.slow && !browser.edge; @@ -41,217 +47,6 @@ export function getCardsHtml(items, options) { return buildCardsHtmlInternal(items, options); } -/** - * Computes the number of posters per row. - * @param {string} shape - Shape of the cards. - * @param {number} screenWidth - Width of the screen. - * @param {boolean} isOrientationLandscape - Flag for the orientation of the screen. - * @returns {number} Number of cards per row for an itemsContainer. - */ -function getPostersPerRow(shape, screenWidth, isOrientationLandscape) { - switch (shape) { - case 'portrait': - if (layoutManager.tv) { - return 100 / 16.66666667; - } - if (screenWidth >= 2200) { - return 100 / 10; - } - if (screenWidth >= 1920) { - return 100 / 11.1111111111; - } - if (screenWidth >= 1600) { - return 100 / 12.5; - } - if (screenWidth >= 1400) { - return 100 / 14.28571428571; - } - if (screenWidth >= 1200) { - return 100 / 16.66666667; - } - if (screenWidth >= 800) { - return 5; - } - if (screenWidth >= 700) { - return 4; - } - if (screenWidth >= 500) { - return 100 / 33.33333333; - } - return 100 / 33.33333333; - case 'square': - if (layoutManager.tv) { - return 100 / 16.66666667; - } - if (screenWidth >= 2200) { - return 100 / 10; - } - if (screenWidth >= 1920) { - return 100 / 11.1111111111; - } - if (screenWidth >= 1600) { - return 100 / 12.5; - } - if (screenWidth >= 1400) { - return 100 / 14.28571428571; - } - if (screenWidth >= 1200) { - return 100 / 16.66666667; - } - if (screenWidth >= 800) { - return 5; - } - if (screenWidth >= 700) { - return 4; - } - if (screenWidth >= 500) { - return 100 / 33.33333333; - } - return 2; - case 'banner': - if (screenWidth >= 2200) { - return 100 / 25; - } - if (screenWidth >= 1200) { - return 100 / 33.33333333; - } - if (screenWidth >= 800) { - return 2; - } - return 1; - case 'backdrop': - if (layoutManager.tv) { - return 100 / 25; - } - if (screenWidth >= 2500) { - return 6; - } - if (screenWidth >= 1600) { - return 5; - } - if (screenWidth >= 1200) { - return 4; - } - if (screenWidth >= 770) { - return 3; - } - if (screenWidth >= 420) { - return 2; - } - return 1; - case 'smallBackdrop': - if (screenWidth >= 1600) { - return 100 / 12.5; - } - if (screenWidth >= 1400) { - return 100 / 14.2857142857; - } - if (screenWidth >= 1200) { - return 100 / 16.66666667; - } - if (screenWidth >= 1000) { - return 5; - } - if (screenWidth >= 800) { - return 4; - } - if (screenWidth >= 500) { - return 100 / 33.33333333; - } - return 2; - case 'overflowSmallBackdrop': - if (layoutManager.tv) { - return 100 / 18.9; - } - if (isOrientationLandscape) { - if (screenWidth >= 800) { - return 100 / 15.5; - } - return 100 / 23.3; - } else { - if (screenWidth >= 540) { - return 100 / 30; - } - return 100 / 72; - } - case 'overflowPortrait': - - if (layoutManager.tv) { - return 100 / 15.5; - } - if (isOrientationLandscape) { - if (screenWidth >= 1700) { - return 100 / 11.6; - } - return 100 / 15.5; - } else { - if (screenWidth >= 1400) { - return 100 / 15; - } - if (screenWidth >= 1200) { - return 100 / 18; - } - if (screenWidth >= 760) { - return 100 / 23; - } - if (screenWidth >= 400) { - return 100 / 31.5; - } - return 100 / 42; - } - case 'overflowSquare': - if (layoutManager.tv) { - return 100 / 15.5; - } - if (isOrientationLandscape) { - if (screenWidth >= 1700) { - return 100 / 11.6; - } - return 100 / 15.5; - } else { - if (screenWidth >= 1400) { - return 100 / 15; - } - if (screenWidth >= 1200) { - return 100 / 18; - } - if (screenWidth >= 760) { - return 100 / 23; - } - if (screenWidth >= 540) { - return 100 / 31.5; - } - return 100 / 42; - } - case 'overflowBackdrop': - if (layoutManager.tv) { - return 100 / 23.3; - } - if (isOrientationLandscape) { - if (screenWidth >= 1700) { - return 100 / 18.5; - } - return 100 / 23.3; - } else { - if (screenWidth >= 1800) { - return 100 / 23.5; - } - if (screenWidth >= 1400) { - return 100 / 30; - } - if (screenWidth >= 760) { - return 100 / 40; - } - if (screenWidth >= 640) { - return 100 / 56; - } - return 100 / 72; - } - default: - return 4; - } -} - /** * Checks if the window is resizable. * @param {number} windowWidth - Width of the device's screen. @@ -278,7 +73,7 @@ function isResizable(windowWidth) { * @returns {number} Width of the image for a card. */ function getImageWidth(shape, screenWidth, isOrientationLandscape) { - const imagesPerRow = getPostersPerRow(shape, screenWidth, isOrientationLandscape); + const imagesPerRow = cardBuilderUtils.getPostersPerRow(shape, screenWidth, isOrientationLandscape, layoutManager.tv); return Math.round(screenWidth / imagesPerRow); } @@ -301,16 +96,16 @@ function setCardData(items, options) { options.shape = 'banner'; options.coverImage = true; } else if (primaryImageAspectRatio >= 1.33) { - options.shape = requestedShape === 'autooverflow' ? 'overflowBackdrop' : 'backdrop'; + options.shape = getBackdropShape(requestedShape === 'autooverflow'); } else if (primaryImageAspectRatio > 0.71) { - options.shape = requestedShape === 'autooverflow' ? 'overflowSquare' : 'square'; + options.shape = getSquareShape(requestedShape === 'autooverflow'); } else { - options.shape = requestedShape === 'autooverflow' ? 'overflowPortrait' : 'portrait'; + options.shape = getPortraitShape(requestedShape === 'autooverflow'); } } if (!options.shape) { - options.shape = options.defaultShape || (requestedShape === 'autooverflow' ? 'overflowSquare' : 'square'); + options.shape = options.defaultShape || getSquareShape(requestedShape === 'autooverflow'); } } @@ -318,7 +113,7 @@ function setCardData(items, options) { options.preferThumb = options.shape === 'backdrop' || options.shape === 'overflowBackdrop'; } - options.uiAspect = getDesiredAspect(options.shape); + options.uiAspect = cardBuilderUtils.getDesiredAspect(options.shape); options.primaryImageAspectRatio = primaryImageAspectRatio; if (!options.width && options.widths) { @@ -460,30 +255,6 @@ function buildCardsHtmlInternal(items, options) { return html; } -/** - * Computes the aspect ratio for a card given its shape. - * @param {string} shape - Shape for which to get the aspect ratio. - * @returns {null|number} Ratio of the shape. - */ -function getDesiredAspect(shape) { - if (shape) { - shape = shape.toLowerCase(); - if (shape.indexOf('portrait') !== -1) { - return (2 / 3); - } - if (shape.indexOf('backdrop') !== -1) { - return (16 / 9); - } - if (shape.indexOf('square') !== -1) { - return 1; - } - if (shape.indexOf('banner') !== -1) { - return (1000 / 185); - } - } - return null; -} - /** * @typedef {Object} CardImageUrl * @property {string} imgUrl - Image URL. @@ -509,7 +280,7 @@ function getCardImageUrl(item, apiClient, options, shape) { let imgUrl = null; let imgTag = null; let coverImage = false; - const uiAspect = getDesiredAspect(shape); + const uiAspect = cardBuilderUtils.getDesiredAspect(shape); let imgType = null; let itemId = null; diff --git a/src/components/cardbuilder/cardBuilderUtils.js b/src/components/cardbuilder/cardBuilderUtils.js new file mode 100644 index 0000000000..494dcaf649 --- /dev/null +++ b/src/components/cardbuilder/cardBuilderUtils.js @@ -0,0 +1,173 @@ +const ASPECT_RATIOS = { + portrait: (2 / 3), + backdrop: (16 / 9), + square: 1, + banner: (1000 / 185) +}; + +/** + * Computes the aspect ratio for a card given its shape. + * @param {string} shape - Shape for which to get the aspect ratio. + * @returns {null|number} Ratio of the shape. + */ +function getDesiredAspect(shape) { + if (!shape) { + return null; + } + + shape = shape.toLowerCase(); + if (shape.indexOf('portrait') !== -1) { + return ASPECT_RATIOS.portrait; + } + if (shape.indexOf('backdrop') !== -1) { + return ASPECT_RATIOS.backdrop; + } + if (shape.indexOf('square') !== -1) { + return ASPECT_RATIOS.square; + } + if (shape.indexOf('banner') !== -1) { + return ASPECT_RATIOS.banner; + } + + return null; +} + +/** + * Computes the number of posters per row. + * @param {string} shape - Shape of the cards. + * @param {number} screenWidth - Width of the screen. + * @param {boolean} isOrientationLandscape - Flag for the orientation of the screen. + * @param {boolean} isTV - Flag to denote if posters are rendered on a television screen. + * @returns {number} Number of cards per row for an itemsContainer. + */ +function getPostersPerRow(shape, screenWidth, isOrientationLandscape, isTV) { + switch (shape) { + case 'portrait': return postersPerRowPortrait(screenWidth, isTV); + case 'square': return postersPerRowSquare(screenWidth, isTV); + case 'banner': return postersPerRowBanner(screenWidth); + case 'backdrop': return postersPerRowBackdrop(screenWidth, isTV); + case 'smallBackdrop': return postersPerRowSmallBackdrop(screenWidth); + case 'overflowSmallBackdrop': return postersPerRowOverflowSmallBackdrop(screenWidth, isOrientationLandscape, isTV); + case 'overflowPortrait': return postersPerRowOverflowPortrait(screenWidth, isOrientationLandscape, isTV); + case 'overflowSquare': return postersPerRowOverflowSquare(screenWidth, isOrientationLandscape, isTV); + case 'overflowBackdrop': return postersPerRowOverflowBackdrop(screenWidth, isOrientationLandscape, isTV); + default: return 4; + } +} + +const postersPerRowPortrait = (screenWidth, isTV) => { + switch (true) { + case isTV: return 100 / 16.66666667; + case screenWidth >= 2200: return 10; + case screenWidth >= 1920: return 100 / 11.1111111111; + case screenWidth >= 1600: return 8; + case screenWidth >= 1400: return 100 / 14.28571428571; + case screenWidth >= 1200: return 100 / 16.66666667; + case screenWidth >= 800: return 5; + case screenWidth >= 700: return 4; + case screenWidth >= 500: return 100 / 33.33333333; + default: return 100 / 33.33333333; + } +}; + +const postersPerRowSquare = (screenWidth, isTV) => { + switch (true) { + case isTV: return 100 / 16.66666667; + case screenWidth >= 2200: return 10; + case screenWidth >= 1920: return 100 / 11.1111111111; + case screenWidth >= 1600: return 8; + case screenWidth >= 1400: return 100 / 14.28571428571; + case screenWidth >= 1200: return 100 / 16.66666667; + case screenWidth >= 800: return 5; + case screenWidth >= 700: return 4; + case screenWidth >= 500: return 100 / 33.33333333; + default: return 2; + } +}; + +const postersPerRowBanner = (screenWidth) => { + switch (true) { + case screenWidth >= 2200: return 4; + case screenWidth >= 1200: return 100 / 33.33333333; + case screenWidth >= 800: return 2; + default: return 1; + } +}; + +const postersPerRowBackdrop = (screenWidth, isTV) => { + switch (true) { + case isTV: return 4; + case screenWidth >= 2500: return 6; + case screenWidth >= 1600: return 5; + case screenWidth >= 1200: return 4; + case screenWidth >= 770: return 3; + case screenWidth >= 420: return 2; + default: return 1; + } +}; + +function postersPerRowSmallBackdrop(screenWidth) { + switch (true) { + case screenWidth >= 1600: return 8; + case screenWidth >= 1400: return 100 / 14.2857142857; + case screenWidth >= 1200: return 100 / 16.66666667; + case screenWidth >= 1000: return 5; + case screenWidth >= 800: return 4; + case screenWidth >= 500: return 100 / 33.33333333; + default: return 2; + } +} + +const postersPerRowOverflowSmallBackdrop = (screenWidth, isLandscape, isTV) => { + switch (true) { + case isTV: return 100 / 18.9; + case isLandscape && screenWidth >= 800: return 100 / 15.5; + case isLandscape: return 100 / 23.3; + case screenWidth >= 540: return 100 / 30; + default: return 100 / 72; + } +}; + +const postersPerRowOverflowPortrait = (screenWidth, isLandscape, isTV) => { + switch (true) { + case isTV: return 100 / 15.5; + case isLandscape && screenWidth >= 1700: return 100 / 11.6; + case isLandscape: return 100 / 15.5; + case screenWidth >= 1400: return 100 / 15; + case screenWidth >= 1200: return 100 / 18; + case screenWidth >= 760: return 100 / 23; + case screenWidth >= 400: return 100 / 31.5; + default: return 100 / 42; + } +}; + +const postersPerRowOverflowSquare = (screenWidth, isLandscape, isTV) => { + switch (true) { + case isTV: return 100 / 15.5; + case isLandscape && screenWidth >= 1700: return 100 / 11.6; + case isLandscape: return 100 / 15.5; + case screenWidth >= 1400: return 100 / 15; + case screenWidth >= 1200: return 100 / 18; + case screenWidth >= 760: return 100 / 23; + case screenWidth >= 540: return 100 / 31.5; + default: return 100 / 42; + } +}; + +const postersPerRowOverflowBackdrop = (screenWidth, isLandscape, isTV) => { + switch (true) { + case isTV: return 100 / 23.3; + case isLandscape && screenWidth >= 1700: return 100 / 18.5; + case isLandscape: return 100 / 23.3; + case screenWidth >= 1800: return 100 / 23.5; + case screenWidth >= 1400: return 100 / 30; + case screenWidth >= 760: return 100 / 40; + case screenWidth >= 640: return 100 / 56; + default: return 100 / 72; + } +}; + +export default { + getDesiredAspect, + getPostersPerRow +}; diff --git a/src/components/cardbuilder/cardBuilderUtils.test.js b/src/components/cardbuilder/cardBuilderUtils.test.js new file mode 100644 index 0000000000..46599135db --- /dev/null +++ b/src/components/cardbuilder/cardBuilderUtils.test.js @@ -0,0 +1,417 @@ +import { describe, expect, test } from 'vitest'; +import cardBuilderUtils from './cardBuilderUtils'; + +describe('getDesiredAspect', () => { + test('"portrait" (case insensitive)', () => { + expect(cardBuilderUtils.getDesiredAspect('portrait')).toEqual((2 / 3)); + expect(cardBuilderUtils.getDesiredAspect('PorTRaIt')).toEqual((2 / 3)); + }); + + test('"backdrop" (case insensitive)', () => { + expect(cardBuilderUtils.getDesiredAspect('backdrop')).toEqual((16 / 9)); + expect(cardBuilderUtils.getDesiredAspect('BaCkDroP')).toEqual((16 / 9)); + }); + + test('"square" (case insensitive)', () => { + expect(cardBuilderUtils.getDesiredAspect('square')).toEqual(1); + expect(cardBuilderUtils.getDesiredAspect('sQuArE')).toEqual(1); + }); + + test('"banner" (case insensitive)', () => { + expect(cardBuilderUtils.getDesiredAspect('banner')).toEqual((1000 / 185)); + expect(cardBuilderUtils.getDesiredAspect('BaNnEr')).toEqual((1000 / 185)); + }); + + test('invalid shape', () => { + expect(cardBuilderUtils.getDesiredAspect('invalid')).toBeNull(); + }); + + test('shape is not provided', () => { + expect(cardBuilderUtils.getDesiredAspect('')).toBeNull(); + }); +}); + +describe('getPostersPerRow', () => { + test('resolves to default of 4 posters per row if shape is not provided', () => { + expect(cardBuilderUtils.getPostersPerRow('', 0, false, false)).toEqual(4); + }); + + describe('portrait', () => { + const postersPerRowForPortrait = (screenWidth, isTV) => (cardBuilderUtils.getPostersPerRow('portrait', screenWidth, false, isTV)); + + test('television', () => { + expect(postersPerRowForPortrait(0, true)).toEqual(100 / 16.66666667); + }); + + test('screen width less than 500px', () => { + expect(postersPerRowForPortrait(100, false)).toEqual(100 / 33.33333333); + expect(postersPerRowForPortrait(499, false)).toEqual(100 / 33.33333333); + }); + + test('screen width greater or equal to 500px', () => { + expect(postersPerRowForPortrait(500, false)).toEqual(100 / 33.33333333); + expect(postersPerRowForPortrait(501, false)).toEqual(100 / 33.33333333); + }); + + test('screen width greater or equal to 700px', () => { + expect(postersPerRowForPortrait(700, false)).toEqual(4); + expect(postersPerRowForPortrait(701, false)).toEqual(4); + }); + + test('screen width greater or equal to 800px', () => { + expect(postersPerRowForPortrait(800, false)).toEqual(5); + expect(postersPerRowForPortrait(801, false)).toEqual(5); + }); + + test('screen width greater or equal to 1200px', () => { + expect(postersPerRowForPortrait(1200, false)).toEqual(100 / 16.66666667); + expect(postersPerRowForPortrait(1201, false)).toEqual(100 / 16.66666667); + }); + + test('screen width greater or equal to 1400px', () => { + expect(postersPerRowForPortrait(1400, false)).toEqual( 100 / 14.28571428571); + expect(postersPerRowForPortrait(1401, false)).toEqual( 100 / 14.28571428571); + }); + + test('screen width greater or equal to 1600px', () => { + expect(postersPerRowForPortrait(1600, false)).toEqual( 8); + expect(postersPerRowForPortrait(1601, false)).toEqual( 8); + }); + + test('screen width greater or equal to 1920px', () => { + expect(postersPerRowForPortrait(1920, false)).toEqual( 100 / 11.1111111111); + expect(postersPerRowForPortrait(1921, false)).toEqual( 100 / 11.1111111111); + }); + + test('screen width greater or equal to 2200px', () => { + expect(postersPerRowForPortrait(2200, false)).toEqual( 10); + expect(postersPerRowForPortrait(2201, false)).toEqual( 10); + }); + }); + + describe('square', () => { + const postersPerRowForSquare = (screenWidth, isTV) => (cardBuilderUtils.getPostersPerRow('square', screenWidth, false, isTV)); + + test('television', () => { + expect(postersPerRowForSquare(0, true)).toEqual(100 / 16.66666667); + }); + + test('screen width less than 500px', () => { + expect(postersPerRowForSquare(100, false)).toEqual(2); + expect(postersPerRowForSquare(499, false)).toEqual(2); + }); + + test('screen width greater or equal to 500px', () => { + expect(postersPerRowForSquare(500, false)).toEqual(100 / 33.33333333); + expect(postersPerRowForSquare(501, false)).toEqual(100 / 33.33333333); + }); + + test('screen width greater or equal to 700px', () => { + expect(postersPerRowForSquare(700, false)).toEqual(4); + expect(postersPerRowForSquare(701, false)).toEqual(4); + }); + + test('screen width greater or equal to 800px', () => { + expect(postersPerRowForSquare(800, false)).toEqual(5); + expect(postersPerRowForSquare(801, false)).toEqual(5); + }); + + test('screen width greater or equal to 1200px', () => { + expect(postersPerRowForSquare(1200, false)).toEqual(100 / 16.66666667); + expect(postersPerRowForSquare(1201, false)).toEqual(100 / 16.66666667); + }); + + test('screen width greater or equal to 1400px', () => { + expect(postersPerRowForSquare(1400, false)).toEqual( 100 / 14.28571428571); + expect(postersPerRowForSquare(1401, false)).toEqual( 100 / 14.28571428571); + }); + + test('screen width greater or equal to 1600px', () => { + expect(postersPerRowForSquare(1600, false)).toEqual(8); + expect(postersPerRowForSquare(1601, false)).toEqual(8); + }); + + test('screen width greater or equal to 1920px', () => { + expect(postersPerRowForSquare(1920, false)).toEqual(100 / 11.1111111111); + expect(postersPerRowForSquare(1921, false)).toEqual(100 / 11.1111111111); + }); + + test('screen width greater or equal to 2200px', () => { + expect(postersPerRowForSquare(2200, false)).toEqual( 10); + expect(postersPerRowForSquare(2201, false)).toEqual( 10); + }); + }); + + describe('banner', () => { + const postersPerRowForBanner = (screenWidth) => (cardBuilderUtils.getPostersPerRow('banner', screenWidth, false, false)); + + test('screen width less than 800px', () => { + expect(postersPerRowForBanner(799)).toEqual(1); + }); + + test('screen width greater than or equal to 800px', () => { + expect(postersPerRowForBanner(800)).toEqual(2); + expect(postersPerRowForBanner(801)).toEqual(2); + }); + + test('screen width greater than or equal to 1200px', () => { + expect(postersPerRowForBanner(1200)).toEqual(100 / 33.33333333); + expect(postersPerRowForBanner(1201)).toEqual(100 / 33.33333333); + }); + + test('screen width greater than or equal to 2200px', () => { + expect(postersPerRowForBanner(2200)).toEqual(4); + expect(postersPerRowForBanner(2201)).toEqual(4); + }); + }); + + describe('backdrop', () => { + const postersPerRowForBackdrop = (screenWidth, isTV) => (cardBuilderUtils.getPostersPerRow('backdrop', screenWidth, false, isTV)); + + test('television', () => { + expect(postersPerRowForBackdrop(0, true)).toEqual(4); + }); + + test('screen width less than 420px', () => { + expect(postersPerRowForBackdrop(100, false)).toEqual(1); + expect(postersPerRowForBackdrop(419, false)).toEqual(1); + }); + + test('screen width greater or equal to 420px', () => { + expect(postersPerRowForBackdrop(420, false)).toEqual(2); + expect(postersPerRowForBackdrop(421, false)).toEqual(2); + }); + + test('screen width greater or equal to 770px', () => { + expect(postersPerRowForBackdrop(770, false)).toEqual(3); + expect(postersPerRowForBackdrop(771, false)).toEqual(3); + }); + + test('screen width greater or equal to 1200px', () => { + expect(postersPerRowForBackdrop(1200, false)).toEqual(4); + expect(postersPerRowForBackdrop(1201, false)).toEqual(4); + }); + + test('screen width greater or equal to 1600px', () => { + expect(postersPerRowForBackdrop(1600, false)).toEqual(5); + expect(postersPerRowForBackdrop(1601, false)).toEqual(5); + }); + + test('screen width greater or equal to 2500px', () => { + expect(postersPerRowForBackdrop(2500, false)).toEqual(6); + expect(postersPerRowForBackdrop(2501, false)).toEqual(6); + }); + }); + + describe('small backdrop', () => { + const postersPerRowForSmallBackdrop = (screenWidth) => (cardBuilderUtils.getPostersPerRow('smallBackdrop', screenWidth, false, false)); + + test('screen width less than 500px', () => { + expect(postersPerRowForSmallBackdrop(100)).toEqual(2); + expect(postersPerRowForSmallBackdrop(499)).toEqual(2); + }); + + test('screen width greater or equal to 500px', () => { + expect(postersPerRowForSmallBackdrop(500)).toEqual(100 / 33.33333333); + expect(postersPerRowForSmallBackdrop(501)).toEqual(100 / 33.33333333); + }); + + test('screen width greater or equal to 800px', () => { + expect(postersPerRowForSmallBackdrop(800)).toEqual(4); + expect(postersPerRowForSmallBackdrop(801)).toEqual(4); + }); + + test('screen width greater or equal to 1000px', () => { + expect(postersPerRowForSmallBackdrop(1000)).toEqual(5); + expect(postersPerRowForSmallBackdrop(1001)).toEqual(5); + }); + + test('screen width greater or equal to 1200px', () => { + expect(postersPerRowForSmallBackdrop(1200)).toEqual(100 / 16.66666667); + expect(postersPerRowForSmallBackdrop(1201)).toEqual(100 / 16.66666667); + }); + + test('screen width greater or equal to 1400px', () => { + expect(postersPerRowForSmallBackdrop(1400)).toEqual(100 / 14.2857142857); + expect(postersPerRowForSmallBackdrop(1401)).toEqual(100 / 14.2857142857); + }); + + test('screen width greater or equal to 1600px', () => { + expect(postersPerRowForSmallBackdrop(1600)).toEqual(8); + expect(postersPerRowForSmallBackdrop(1601)).toEqual(8); + }); + }); + + describe('overflow small backdrop', () => { + const postersPerRowForOverflowSmallBackdrop = (screenWidth, isLandscape, isTV) => (cardBuilderUtils.getPostersPerRow('overflowSmallBackdrop', screenWidth, isLandscape, isTV)); + + test('television', () => { + expect(postersPerRowForOverflowSmallBackdrop(0, false, true)).toEqual( 100 / 18.9); + }); + + describe('non-landscape', () => { + test('screen width greater or equal to 540px', () => { + expect(postersPerRowForOverflowSmallBackdrop(540, false)).toEqual(100 / 30); + expect(postersPerRowForOverflowSmallBackdrop(541, false)).toEqual(100 / 30); + }); + + test('screen width is less than 540px', () => { + expect(postersPerRowForOverflowSmallBackdrop(539, false)).toEqual(100 / 72); + expect(postersPerRowForOverflowSmallBackdrop(100, false)).toEqual(100 / 72); + }); + }); + + describe('landscape', () => { + test('screen width greater or equal to 800px', () => { + expect(postersPerRowForOverflowSmallBackdrop(800, true)).toEqual(100 / 15.5); + expect(postersPerRowForOverflowSmallBackdrop(801, true)).toEqual(100 / 15.5); + }); + + test('screen width is less than 800px', () => { + expect(postersPerRowForOverflowSmallBackdrop(799, true)).toEqual(100 / 23.3); + expect(postersPerRowForOverflowSmallBackdrop(100, true)).toEqual(100 / 23.3); + }); + }); + }); + + describe('overflow portrait', () => { + const postersPerRowForOverflowPortrait = (screenWidth, isLandscape, isTV) => (cardBuilderUtils.getPostersPerRow('overflowPortrait', screenWidth, isLandscape, isTV)); + + test('television', () => { + expect(postersPerRowForOverflowPortrait(0, false, true)).toEqual( 100 / 15.5); + }); + + describe('non-landscape', () => { + test('screen width greater or equal to 1400px', () => { + expect(postersPerRowForOverflowPortrait(1400, false)).toEqual(100 / 15); + expect(postersPerRowForOverflowPortrait(1401, false)).toEqual(100 / 15); + }); + + test('screen width greater or equal to 1200px', () => { + expect(postersPerRowForOverflowPortrait(1200, false)).toEqual(100 / 18); + expect(postersPerRowForOverflowPortrait(1201, false)).toEqual(100 / 18); + }); + + test('screen width greater or equal to 760px', () => { + expect(postersPerRowForOverflowPortrait(760, false)).toEqual(100 / 23); + expect(postersPerRowForOverflowPortrait(761, false)).toEqual(100 / 23); + }); + + test('screen width greater or equal to 400px', () => { + expect(postersPerRowForOverflowPortrait(400, false)).toEqual(100 / 31.5); + expect(postersPerRowForOverflowPortrait(401, false)).toEqual(100 / 31.5); + }); + + test('screen width is less than 400px', () => { + expect(postersPerRowForOverflowPortrait(399, false)).toEqual(100 / 42); + expect(postersPerRowForOverflowPortrait(100, false)).toEqual(100 / 42); + }); + }); + + describe('landscape', () => { + test('screen width greater or equal to 1700px', () => { + expect(postersPerRowForOverflowPortrait(1700, true)).toEqual(100 / 11.6); + expect(postersPerRowForOverflowPortrait(1701, true)).toEqual(100 / 11.6); + }); + + test('screen width is less than 1700px', () => { + expect(postersPerRowForOverflowPortrait(1699, true)).toEqual(100 / 15.5); + expect(postersPerRowForOverflowPortrait(100, true)).toEqual(100 / 15.5); + }); + }); + }); + + describe('overflow square', () => { + const postersPerRowForOverflowSquare = (screenWidth, isLandscape, isTV) => (cardBuilderUtils.getPostersPerRow('overflowSquare', screenWidth, isLandscape, isTV)); + + test('television', () => { + expect(postersPerRowForOverflowSquare(0, false, true)).toEqual( 100 / 15.5); + }); + + describe('non-landscape', () => { + test('screen width greater or equal to 1400px', () => { + expect(postersPerRowForOverflowSquare(1400, false)).toEqual(100 / 15); + expect(postersPerRowForOverflowSquare(1401, false)).toEqual(100 / 15); + }); + + test('screen width greater or equal to 1200px', () => { + expect(postersPerRowForOverflowSquare(1200, false)).toEqual(100 / 18); + expect(postersPerRowForOverflowSquare(1201, false)).toEqual(100 / 18); + }); + + test('screen width greater or equal to 760px', () => { + expect(postersPerRowForOverflowSquare(760, false)).toEqual(100 / 23); + expect(postersPerRowForOverflowSquare(761, false)).toEqual(100 / 23); + }); + + test('screen width greater or equal to 540px', () => { + expect(postersPerRowForOverflowSquare(540, false)).toEqual(100 / 31.5); + expect(postersPerRowForOverflowSquare(541, false)).toEqual(100 / 31.5); + }); + + test('screen width is less than 540px', () => { + expect(postersPerRowForOverflowSquare(539, false)).toEqual(100 / 42); + expect(postersPerRowForOverflowSquare(100, false)).toEqual(100 / 42); + }); + }); + + describe('landscape', () => { + test('screen width greater or equal to 1700px', () => { + expect(postersPerRowForOverflowSquare(1700, true)).toEqual(100 / 11.6); + expect(postersPerRowForOverflowSquare(1701, true)).toEqual(100 / 11.6); + }); + + test('screen width is less than 1700px', () => { + expect(postersPerRowForOverflowSquare(1699, true)).toEqual(100 / 15.5); + expect(postersPerRowForOverflowSquare(100, true)).toEqual(100 / 15.5); + }); + }); + }); + + describe('overflow backdrop', () => { + const postersPerRowForOverflowBackdrop = (screenWidth, isLandscape, isTV) => (cardBuilderUtils.getPostersPerRow('overflowBackdrop', screenWidth, isLandscape, isTV)); + + test('television', () => { + expect(postersPerRowForOverflowBackdrop(0, false, true)).toEqual( 100 / 23.3); + }); + + describe('non-landscape', () => { + test('screen width greater or equal to 1800px', () => { + expect(postersPerRowForOverflowBackdrop(1800, false)).toEqual(100 / 23.5); + expect(postersPerRowForOverflowBackdrop(1801, false)).toEqual(100 / 23.5); + }); + + test('screen width greater or equal to 1400px', () => { + expect(postersPerRowForOverflowBackdrop(1400, false)).toEqual(100 / 30); + expect(postersPerRowForOverflowBackdrop(1401, false)).toEqual(100 / 30); + }); + + test('screen width greater or equal to 760px', () => { + expect(postersPerRowForOverflowBackdrop(760, false)).toEqual(100 / 40); + expect(postersPerRowForOverflowBackdrop(761, false)).toEqual(100 / 40); + }); + + test('screen width greater or equal to 640px', () => { + expect(postersPerRowForOverflowBackdrop(640, false)).toEqual(100 / 56); + expect(postersPerRowForOverflowBackdrop(641, false)).toEqual(100 / 56); + }); + + test('screen width is less than 640px', () => { + expect(postersPerRowForOverflowBackdrop(639, false)).toEqual(100 / 72); + expect(postersPerRowForOverflowBackdrop(100, false)).toEqual(100 / 72); + }); + }); + + describe('landscape', () => { + test('screen width greater or equal to 1700px', () => { + expect(postersPerRowForOverflowBackdrop(1700, true)).toEqual(100 / 18.5); + expect(postersPerRowForOverflowBackdrop(1701, true)).toEqual(100 / 18.5); + }); + + test('screen width is less than 1700px', () => { + expect(postersPerRowForOverflowBackdrop(1699, true)).toEqual(100 / 23.3); + expect(postersPerRowForOverflowBackdrop(100, true)).toEqual(100 / 23.3); + }); + }); + }); +}); diff --git a/src/components/dashboard/users/SectionTabs.tsx b/src/components/dashboard/users/SectionTabs.tsx index 0fad3469df..1befb5912b 100644 --- a/src/components/dashboard/users/SectionTabs.tsx +++ b/src/components/dashboard/users/SectionTabs.tsx @@ -10,28 +10,28 @@ const createLinkElement = (activeTab: string) => ({ is="emby-linkbutton" data-role="button" class="${activeTab === 'useredit' ? 'ui-btn-active' : ''}" - onclick="Dashboard.navigate('useredit.html', true);"> + onclick="Dashboard.navigate('/dashboard/users/profile', true);"> ${globalize.translate('Profile')} + onclick="Dashboard.navigate('/dashboard/users/access', true);"> ${globalize.translate('TabAccess')} + onclick="Dashboard.navigate('/dashboard/users/parentalcontrol', true);"> ${globalize.translate('TabParentalControl')} + onclick="Dashboard.navigate('/dashboard/users/password', true);"> ${globalize.translate('HeaderPassword')} ` }); diff --git a/src/components/dashboard/users/UserCardBox.tsx b/src/components/dashboard/users/UserCardBox.tsx index b535f7a16d..f0fbdf96a7 100644 --- a/src/components/dashboard/users/UserCardBox.tsx +++ b/src/components/dashboard/users/UserCardBox.tsx @@ -11,7 +11,7 @@ const createLinkElement = ({ user, renderImgUrl }: { user: UserDto, renderImgUrl __html: ` ${renderImgUrl} ` diff --git a/src/components/displaySettings/displaySettings.js b/src/components/displaySettings/displaySettings.js index 9103011ac9..99c1ab3e2c 100644 --- a/src/components/displaySettings/displaySettings.js +++ b/src/components/displaySettings/displaySettings.js @@ -36,7 +36,7 @@ function loadScreensavers(context, userSettings) { const selectScreensaver = context.querySelector('.selectScreensaver'); const options = pluginManager.ofType(PluginType.Screensaver).map(plugin => { return { - name: plugin.name, + name: globalize.translate(plugin.name), value: plugin.id }; }); diff --git a/src/components/favoriteitems.js b/src/components/favoriteitems.js index ac0f3c0de0..b26c25ede1 100644 --- a/src/components/favoriteitems.js +++ b/src/components/favoriteitems.js @@ -1,50 +1,42 @@ -import loading from './loading/loading'; -import cardBuilder from './cardbuilder/cardBuilder'; -import dom from '../scripts/dom'; +import dom from 'scripts/dom'; +import globalize from 'scripts/globalize'; +import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card'; +import { getParameterByName } from 'utils/url'; + import { appHost } from './apphost'; +import cardBuilder from './cardbuilder/cardBuilder'; import imageLoader from './images/imageLoader'; -import globalize from '../scripts/globalize'; import layoutManager from './layoutManager'; -import { getParameterByName } from '../utils/url.ts'; -import '../styles/scrollstyles.scss'; -import '../elements/emby-itemscontainer/emby-itemscontainer'; +import loading from './loading/loading'; + +import 'elements/emby-itemscontainer/emby-itemscontainer'; + +import 'styles/scrollstyles.scss'; function enableScrollX() { return !layoutManager.desktop; } -function getThumbShape() { - return enableScrollX() ? 'overflowBackdrop' : 'backdrop'; -} - -function getPosterShape() { - return enableScrollX() ? 'overflowPortrait' : 'portrait'; -} - -function getSquareShape() { - return enableScrollX() ? 'overflowSquare' : 'square'; -} - function getSections() { return [{ name: 'Movies', types: 'Movie', id: 'favoriteMovies', - shape: getPosterShape(), + shape: getPortraitShape(enableScrollX()), showTitle: false, overlayPlayButton: true }, { name: 'Shows', types: 'Series', id: 'favoriteShows', - shape: getPosterShape(), + shape: getPortraitShape(enableScrollX()), showTitle: false, overlayPlayButton: true }, { name: 'Episodes', types: 'Episode', id: 'favoriteEpisode', - shape: getThumbShape(), + shape: getBackdropShape(enableScrollX()), preferThumb: false, showTitle: true, showParentTitle: true, @@ -55,7 +47,7 @@ function getSections() { name: 'Videos', types: 'Video,MusicVideo', id: 'favoriteVideos', - shape: getThumbShape(), + shape: getBackdropShape(enableScrollX()), preferThumb: true, showTitle: true, overlayPlayButton: true, @@ -65,7 +57,7 @@ function getSections() { name: 'Artists', types: 'MusicArtist', id: 'favoriteArtists', - shape: getSquareShape(), + shape: getSquareShape(enableScrollX()), preferThumb: false, showTitle: true, overlayText: false, @@ -77,7 +69,7 @@ function getSections() { name: 'Albums', types: 'MusicAlbum', id: 'favoriteAlbums', - shape: getSquareShape(), + shape: getSquareShape(enableScrollX()), preferThumb: false, showTitle: true, overlayText: false, @@ -89,7 +81,7 @@ function getSections() { name: 'Songs', types: 'Audio', id: 'favoriteSongs', - shape: getSquareShape(), + shape: getSquareShape(enableScrollX()), preferThumb: false, showTitle: true, overlayText: false, diff --git a/src/components/homesections/homesections.js b/src/components/homesections/homesections.js index 1b26f8624a..27c48f0998 100644 --- a/src/components/homesections/homesections.js +++ b/src/components/homesections/homesections.js @@ -1,18 +1,23 @@ import escapeHtml from 'escape-html'; + +import globalize from 'scripts/globalize'; +import imageHelper from 'scripts/imagehelper'; +import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card'; +import Dashboard from 'utils/dashboard'; + import cardBuilder from '../cardbuilder/cardBuilder'; -import layoutManager from '../layoutManager'; import imageLoader from '../images/imageLoader'; -import globalize from '../../scripts/globalize'; +import layoutManager from '../layoutManager'; import { appRouter } from '../router/appRouter'; -import imageHelper from '../../scripts/imagehelper'; -import '../../elements/emby-button/paper-icon-button-light'; -import '../../elements/emby-itemscontainer/emby-itemscontainer'; -import '../../elements/emby-scroller/emby-scroller'; -import '../../elements/emby-button/emby-button'; -import './homesections.scss'; -import Dashboard from '../../utils/dashboard'; import ServerConnections from '../ServerConnections'; +import 'elements/emby-button/paper-icon-button-light'; +import 'elements/emby-itemscontainer/emby-itemscontainer'; +import 'elements/emby-scroller/emby-scroller'; +import 'elements/emby-button/emby-button'; + +import './homesections.scss'; + export function getDefaultSection(index) { switch (index) { case 0: @@ -69,11 +74,14 @@ export function loadSections(elem, apiClient, user, userSettings) { promises.push(loadSection(elem, apiClient, user, userSettings, userViews, sections, i)); } - return Promise.all(promises).then(function () { - return resume(elem, { - refresh: true + return Promise.all(promises) + // Timeout for polyfilled CustomElements (webOS 1.2) + .then(() => new Promise((resolve) => setTimeout(resolve, 0))) + .then(() => { + return resume(elem, { + refresh: true + }); }); - }); } else { let noLibDescription; if (user.Policy?.IsAdministrator) { @@ -91,7 +99,7 @@ export function loadSections(elem, apiClient, user, userSettings) { const createNowLink = elem.querySelector('#button-createLibrary'); if (createNowLink) { createNowLink.addEventListener('click', function () { - Dashboard.navigate('library.html'); + Dashboard.navigate('dashboard/libraries'); }); } } @@ -166,18 +174,6 @@ function enableScrollX() { return true; } -function getSquareShape() { - return enableScrollX() ? 'overflowSquare' : 'square'; -} - -function getThumbShape() { - return enableScrollX() ? 'overflowBackdrop' : 'backdrop'; -} - -function getPortraitShape() { - return enableScrollX() ? 'overflowPortrait' : 'portrait'; -} - function getLibraryButtonsHtml(items) { let html = ''; @@ -241,11 +237,11 @@ function getLatestItemsHtmlFn(itemType, viewType) { const cardLayout = false; let shape; if (itemType === 'Channel' || viewType === 'movies' || viewType === 'books' || viewType === 'tvshows') { - shape = getPortraitShape(); + shape = getPortraitShape(enableScrollX()); } else if (viewType === 'music' || viewType === 'homevideos') { - shape = getSquareShape(); + shape = getSquareShape(enableScrollX()); } else { - shape = getThumbShape(); + shape = getBackdropShape(enableScrollX()); } return cardBuilder.getCardsHtml({ @@ -342,7 +338,7 @@ export function loadLibraryTiles(elem, apiClient, user, userSettings, shape, use html += cardBuilder.getCardsHtml({ items: userViews, - shape: getThumbShape(), + shape: getBackdropShape(enableScrollX()), showTitle: true, centerText: true, overlayText: false, @@ -420,7 +416,9 @@ function getItemsToResumeHtmlFn(useEpisodeImages, mediaType) { items: items, preferThumb: true, inheritThumb: !useEpisodeImages, - shape: (mediaType === 'Book') ? getPortraitShape() : getThumbShape(), + shape: (mediaType === 'Book') ? + getPortraitShape(enableScrollX()) : + getBackdropShape(enableScrollX()), overlayText: false, showTitle: true, showParentTitle: true, @@ -468,7 +466,7 @@ function getOnNowItemsHtml(items) { showChannelName: false, showAirDateTime: false, showAirEndTime: true, - defaultShape: getThumbShape(), + defaultShape: getBackdropShape(enableScrollX()), lines: 3, overlayPlayButton: true }); @@ -611,7 +609,7 @@ function getNextUpItemsHtmlFn(useEpisodeImages) { items: items, preferThumb: true, inheritThumb: !useEpisodeImages, - shape: getThumbShape(), + shape: getBackdropShape(enableScrollX()), overlayText: false, showTitle: true, showParentTitle: true, diff --git a/src/components/itemContextMenu.js b/src/components/itemContextMenu.js index 89ef6b3d28..bf03b69039 100644 --- a/src/components/itemContextMenu.js +++ b/src/components/itemContextMenu.js @@ -445,7 +445,7 @@ function executeCommand(item, id, options) { }); break; case 'multiSelect': - import('./multiSelect/multiSelect').then(({ startMultiSelect: startMultiSelect }) => { + import('./multiSelect/multiSelect').then(({ startMultiSelect }) => { const card = dom.parentWithClass(options.positionTo, 'card'); startMultiSelect(card); }); diff --git a/src/components/router/AsyncRoute.tsx b/src/components/router/AsyncRoute.tsx index 9ed49544fb..031e5700ea 100644 --- a/src/components/router/AsyncRoute.tsx +++ b/src/components/router/AsyncRoute.tsx @@ -1,4 +1,4 @@ -import loadable from '@loadable/component'; +import loadable, { LoadableComponent } from '@loadable/component'; import React from 'react'; import { Route } from 'react-router-dom'; @@ -10,13 +10,18 @@ export enum AsyncRouteType { export interface AsyncRoute { /** The URL path for this route. */ path: string - /** The relative path to the page component in the routes directory. */ - page: string - /** The route should use the page component from the experimental app. */ + /** + * The relative path to the page component in the routes directory. + * Will fallback to using the `path` value if not specified. + */ + page?: string + /** The page element to render. */ + element?: LoadableComponent + /** The page type used to load the correct page element. */ type?: AsyncRouteType } -interface AsyncPageProps { +export interface AsyncPageProps { /** The relative path to the page component in the routes directory. */ page: string } @@ -31,14 +36,19 @@ const StableAsyncPage = loadable( { cacheKey: (props: AsyncPageProps) => props.page } ); -export const toAsyncPageRoute = ({ path, page, type = AsyncRouteType.Stable }: AsyncRoute) => ( - { + const Element = element + || ( type === AsyncRouteType.Experimental ? - : - - )} - /> -); + ExperimentalAsyncPage : + StableAsyncPage + ); + + return ( + } + /> + ); +}; diff --git a/src/components/router/Redirect.tsx b/src/components/router/Redirect.tsx new file mode 100644 index 0000000000..7354f16c55 --- /dev/null +++ b/src/components/router/Redirect.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { Navigate, Route, useLocation } from 'react-router-dom'; + +export interface Redirect { + from: string + to: string +} + +const RedirectWithSearch = ({ to }: { to: string }) => { + const { search } = useLocation(); + + return ( + + ); +}; + +export function toRedirectRoute({ from, to }: Redirect) { + return ( + } + /> + ); +} diff --git a/src/components/router/appRouter.js b/src/components/router/appRouter.js index 253fdee922..c56d7fc6be 100644 --- a/src/components/router/appRouter.js +++ b/src/components/router/appRouter.js @@ -527,7 +527,7 @@ class AppRouter { } if (item === 'manageserver') { - return '#/dashboard.html'; + return '#/dashboard'; } if (item === 'recordedtv') { diff --git a/src/components/subtitlesync/subtitlesync.js b/src/components/subtitlesync/subtitlesync.js index ba8984486b..57ae63cb8c 100644 --- a/src/components/subtitlesync/subtitlesync.js +++ b/src/components/subtitlesync/subtitlesync.js @@ -46,15 +46,8 @@ function init(instance) { if (inputOffset) { inputOffset = inputOffset[0]; inputOffset = parseFloat(inputOffset); - inputOffset = Math.min(30, Math.max(-30, inputOffset)); - // replace current text by considered offset - this.textContent = inputOffset + 's'; - // set new offset - playbackManager.setSubtitleOffset(inputOffset, player); - // synchronize with slider value - subtitleSyncSlider.updateOffset( - getSliderValueFromOffset(inputOffset)); + subtitleSyncSlider.updateOffset(inputOffset); } else { this.textContent = (playbackManager.getPlayerSubtitleOffset(player) || 0) + 's'; } @@ -79,23 +72,26 @@ function init(instance) { } }; + function updateSubtitleOffset() { + const value = parseFloat(subtitleSyncSlider.value); + // set new offset + playbackManager.setSubtitleOffset(value, player); + // synchronize with textField value + subtitleSyncTextField.updateOffset(value); + } + subtitleSyncSlider.updateOffset = function (sliderValue) { // default value is 0s = 0ms this.value = sliderValue === undefined ? 0 : sliderValue; + + updateSubtitleOffset(); }; - subtitleSyncSlider.addEventListener('change', function () { - // set new offset - playbackManager.setSubtitleOffset(getOffsetFromSliderValue(this.value), player); - // synchronize with textField value - subtitleSyncTextField.updateOffset( - getOffsetFromSliderValue(this.value)); - }); + subtitleSyncSlider.addEventListener('change', () => updateSubtitleOffset()); - subtitleSyncSlider.getBubbleHtml = function (value) { - const newOffset = getOffsetFromPercentage(value); + subtitleSyncSlider.getBubbleHtml = function (_, value) { return '

' - + (newOffset > 0 ? '+' : '') + parseFloat(newOffset) + 's' + + (value > 0 ? '+' : '') + parseFloat(value) + 's' + '

'; }; @@ -107,25 +103,6 @@ function init(instance) { instance.element = parent; } -function getOffsetFromPercentage(value) { - // convert percentage to fraction - let offset = (value - 50) / 50; - // multiply by offset min/max range value (-x to +x) : - offset *= 30; - return offset.toFixed(1); -} - -function getOffsetFromSliderValue(value) { - // convert slider value to offset - const offset = value / 10; - return offset.toFixed(1); -} - -function getSliderValueFromOffset(value) { - const sliderValue = value * 10; - return Math.min(300, Math.max(-300, sliderValue.toFixed(1))); -} - class SubtitleSync { constructor(currentPlayer) { player = currentPlayer; diff --git a/src/components/subtitlesync/subtitlesync.template.html b/src/components/subtitlesync/subtitlesync.template.html index a055d24fcb..eb36d58260 100644 --- a/src/components/subtitlesync/subtitlesync.template.html +++ b/src/components/subtitlesync/subtitlesync.template.html @@ -3,7 +3,7 @@
0s
- +
diff --git a/src/components/subtitleuploader/subtitleuploader.js b/src/components/subtitleuploader/subtitleuploader.js index 90e04b16dd..6cd3f1ff25 100644 --- a/src/components/subtitleuploader/subtitleuploader.js +++ b/src/components/subtitleuploader/subtitleuploader.js @@ -1,5 +1,7 @@ import escapeHtml from 'escape-html'; +import { getSubtitleApi } from '@jellyfin/sdk/lib/utils/api/subtitle-api'; +import { toApi } from 'utils/jellyfin-apiclient/compat'; import dialogHelper from '../../components/dialogHelper/dialogHelper'; import ServerConnections from '../ServerConnections'; import dom from '../../scripts/dom'; @@ -13,6 +15,7 @@ import '../../elements/emby-button/emby-button'; import '../../elements/emby-select/emby-select'; import '../formdialog.scss'; import './style.scss'; +import { readFileAsBase64 } from 'utils/file'; let currentItemId; let currentServerId; @@ -75,7 +78,7 @@ function setFiles(page, files) { reader.readAsDataURL(file); } -function onSubmit(e) { +async function onSubmit(e) { const file = currentFile; if (!isValidSubtitleFile(file)) { @@ -89,8 +92,17 @@ function onSubmit(e) { const dlg = dom.parentWithClass(this, 'dialog'); const language = dlg.querySelector('#selectLanguage').value; const isForced = dlg.querySelector('#chkIsForced').checked; + const isHearingImpaired = dlg.querySelector('#chkIsHearingImpaired').checked; - ServerConnections.getApiClient(currentServerId).uploadItemSubtitle(currentItemId, language, isForced, file).then(function () { + const subtitleApi = getSubtitleApi(toApi(ServerConnections.getApiClient(currentServerId))); + + const data = await readFileAsBase64(file); + const format = file.name.substring(file.name.lastIndexOf('.') + 1).toLowerCase(); + + subtitleApi.uploadSubtitle({ + itemId: currentItemId, + uploadSubtitleDto: { Data: data, Language: language, IsForced: isForced, Format: format, IsHearingImpaired: isHearingImpaired } + }).then(function () { dlg.querySelector('#uploadSubtitle').value = ''; loading.hide(); hasChanges = true; diff --git a/src/components/subtitleuploader/subtitleuploader.template.html b/src/components/subtitleuploader/subtitleuploader.template.html index ba43e00411..7febd1e7ac 100644 --- a/src/components/subtitleuploader/subtitleuploader.template.html +++ b/src/components/subtitleuploader/subtitleuploader.template.html @@ -31,6 +31,10 @@ ${LabelIsForced} +
diff --git a/src/components/toolbar/AppToolbar.tsx b/src/components/toolbar/AppToolbar.tsx new file mode 100644 index 0000000000..8ed92eee16 --- /dev/null +++ b/src/components/toolbar/AppToolbar.tsx @@ -0,0 +1,129 @@ +import ArrowBack from '@mui/icons-material/ArrowBack'; +import MenuIcon from '@mui/icons-material/Menu'; +import Box from '@mui/material/Box'; +import IconButton from '@mui/material/IconButton'; +import Toolbar from '@mui/material/Toolbar'; +import Tooltip from '@mui/material/Tooltip'; +import Typography from '@mui/material/Typography'; +import React, { FC, ReactNode } from 'react'; +import { Link } from 'react-router-dom'; + +import appIcon from 'assets/img/icon-transparent.png'; +import { appRouter } from 'components/router/appRouter'; +import { useApi } from 'hooks/useApi'; +import globalize from 'scripts/globalize'; + +import UserMenuButton from './UserMenuButton'; + +interface AppToolbarProps { + buttons?: ReactNode + isDrawerAvailable: boolean + isDrawerOpen: boolean + onDrawerButtonClick: (event: React.MouseEvent) => void +} + +const onBackButtonClick = () => { + appRouter.back() + .catch(err => { + console.error('[AppToolbar] error calling appRouter.back', err); + }); +}; + +const AppToolbar: FC = ({ + buttons, + children, + isDrawerAvailable, + isDrawerOpen, + onDrawerButtonClick +}) => { + const { user } = useApi(); + const isUserLoggedIn = Boolean(user); + + const isBackButtonAvailable = appRouter.canGoBack(); + + return ( + + {isUserLoggedIn && isDrawerAvailable && ( + + + + + + )} + + {isBackButtonAvailable && ( + + + + + + )} + + + + + Jellyfin + + + + {children} + + {isUserLoggedIn && ( + <> + + {buttons} + + + + + + + )} + + ); +}; + +export default AppToolbar; diff --git a/src/apps/experimental/components/AppToolbar/menus/AppUserMenu.tsx b/src/components/toolbar/AppUserMenu.tsx similarity index 97% rename from src/apps/experimental/components/AppToolbar/menus/AppUserMenu.tsx rename to src/components/toolbar/AppUserMenu.tsx index 54fdd3ae62..634d02cf88 100644 --- a/src/apps/experimental/components/AppToolbar/menus/AppUserMenu.tsx +++ b/src/components/toolbar/AppUserMenu.tsx @@ -115,7 +115,7 @@ const AppUserMenu: FC = ({ @@ -127,7 +127,7 @@ const AppUserMenu: FC = ({ @@ -140,7 +140,7 @@ const AppUserMenu: FC = ({ diff --git a/src/apps/experimental/components/AppToolbar/UserMenuButton.tsx b/src/components/toolbar/UserMenuButton.tsx similarity index 96% rename from src/apps/experimental/components/AppToolbar/UserMenuButton.tsx rename to src/components/toolbar/UserMenuButton.tsx index 89f18669e9..fa47ce85e6 100644 --- a/src/apps/experimental/components/AppToolbar/UserMenuButton.tsx +++ b/src/components/toolbar/UserMenuButton.tsx @@ -2,11 +2,11 @@ import IconButton from '@mui/material/IconButton'; import Tooltip from '@mui/material/Tooltip'; import React, { useCallback, useState } from 'react'; +import UserAvatar from 'components/UserAvatar'; import { useApi } from 'hooks/useApi'; import globalize from 'scripts/globalize'; -import AppUserMenu, { ID } from './menus/AppUserMenu'; -import UserAvatar from 'components/UserAvatar'; +import AppUserMenu, { ID } from './AppUserMenu'; const UserMenuButton = () => { const { user } = useApi(); diff --git a/src/components/viewContainer.js b/src/components/viewContainer.js index c2156a7d58..b5509f8b5a 100644 --- a/src/components/viewContainer.js +++ b/src/components/viewContainer.js @@ -3,11 +3,7 @@ import './viewManager/viewContainer.scss'; import Dashboard from '../utils/dashboard'; const getMainAnimatedPages = () => { - if (!mainAnimatedPages) { - mainAnimatedPages = document.querySelector('.mainAnimatedPages'); - } - - return mainAnimatedPages; + return document.querySelector('.mainAnimatedPages'); }; function setControllerClass(view, options) { @@ -61,7 +57,9 @@ export function loadView(options) { view.classList.add('mainAnimatedPage'); - if (!getMainAnimatedPages()) { + const mainAnimatedPages = getMainAnimatedPages(); + + if (!mainAnimatedPages) { console.warn('[viewContainer] main animated pages element is not present'); return; } @@ -187,6 +185,7 @@ export function setOnBeforeChange(fn) { } export function tryRestoreView(options) { + console.debug('[viewContainer] tryRestoreView', options); const url = options.url; const index = currentUrls.indexOf(url); @@ -232,14 +231,15 @@ function triggerDestroy(view) { } export function reset() { + console.debug('[viewContainer] resetting view cache'); allPages = []; currentUrls = []; + const mainAnimatedPages = getMainAnimatedPages(); if (mainAnimatedPages) mainAnimatedPages.innerHTML = ''; selectedPageIndex = -1; } let onBeforeChange; -let mainAnimatedPages; let allPages = []; let currentUrls = []; const pageContainerCount = 3; @@ -248,8 +248,8 @@ reset(); getMainAnimatedPages()?.classList.remove('hide'); export default { - loadView: loadView, - tryRestoreView: tryRestoreView, - reset: reset, - setOnBeforeChange: setOnBeforeChange + loadView, + tryRestoreView, + reset, + setOnBeforeChange }; diff --git a/src/controllers/dashboard/dashboard.html b/src/controllers/dashboard/dashboard.html index a76cf11971..aed6bb206f 100644 --- a/src/controllers/dashboard/dashboard.html +++ b/src/controllers/dashboard/dashboard.html @@ -3,7 +3,7 @@
- +

${TabServer}

@@ -33,7 +33,7 @@
- +

${HeaderActiveDevices}

@@ -70,7 +70,7 @@
- +

${HeaderPaths}

diff --git a/src/controllers/dashboard/devices/devices.js b/src/controllers/dashboard/devices/devices.js index 1c5ede9531..c62571a1bc 100644 --- a/src/controllers/dashboard/devices/devices.js +++ b/src/controllers/dashboard/devices/devices.js @@ -73,7 +73,7 @@ function showDeviceMenu(view, btn, deviceId) { callback: function (id) { switch (id) { case 'open': - Dashboard.navigate('device.html?id=' + deviceId); + Dashboard.navigate('dashboard/devices/edit?id=' + deviceId); break; case 'delete': @@ -94,7 +94,7 @@ function load(page, devices) { deviceHtml += '
'; deviceHtml += ' diff --git a/src/controllers/dashboard/dlna/profile.js b/src/controllers/dashboard/dlna/profile.js index 0a88f4214e..0f92a3200c 100644 --- a/src/controllers/dashboard/dlna/profile.js +++ b/src/controllers/dashboard/dlna/profile.js @@ -639,7 +639,7 @@ function saveProfile(page, profile) { data: JSON.stringify(profile), contentType: 'application/json' }).then(function () { - Dashboard.navigate('dlnaprofiles.html'); + Dashboard.navigate('dashboard/dlna/profiles'); }, Dashboard.processErrorResponse); } diff --git a/src/controllers/dashboard/dlna/profiles.html b/src/controllers/dashboard/dlna/profiles.html index 6eb60d1c34..f1696632c9 100644 --- a/src/controllers/dashboard/dlna/profiles.html +++ b/src/controllers/dashboard/dlna/profiles.html @@ -8,7 +8,7 @@
diff --git a/src/controllers/dashboard/dlna/profiles.js b/src/controllers/dashboard/dlna/profiles.js index a7c8f045c4..f69a0c6bf0 100644 --- a/src/controllers/dashboard/dlna/profiles.js +++ b/src/controllers/dashboard/dlna/profiles.js @@ -40,7 +40,7 @@ function renderProfiles(page, element, profiles) { html += '
'; html += ''; html += ''; @@ -78,10 +78,10 @@ function deleteProfile(page, id) { function getTabs() { return [{ - href: '#/dlnasettings.html', + href: '#/dashboard/dlna', name: globalize.translate('Settings') }, { - href: '#/dlnaprofiles.html', + href: '#/dashboard/dlna/profiles', name: globalize.translate('TabProfiles') }]; } diff --git a/src/controllers/dashboard/dlna/settings.js b/src/controllers/dashboard/dlna/settings.js index fcc7d7d100..d12b6744af 100644 --- a/src/controllers/dashboard/dlna/settings.js +++ b/src/controllers/dashboard/dlna/settings.js @@ -37,10 +37,10 @@ function onSubmit() { function getTabs() { return [{ - href: '#/dlnasettings.html', + href: '#/dashboard/dlna', name: globalize.translate('Settings') }, { - href: '#/dlnaprofiles.html', + href: '#/dashboard/dlna/profiles', name: globalize.translate('TabProfiles') }]; } diff --git a/src/controllers/dashboard/encodingsettings.js b/src/controllers/dashboard/encodingsettings.js index 9c268c0866..24946cadb7 100644 --- a/src/controllers/dashboard/encodingsettings.js +++ b/src/controllers/dashboard/encodingsettings.js @@ -167,13 +167,13 @@ function setDecodingCodecsVisible(context, value) { function getTabs() { return [{ - href: '#/encodingsettings.html', + href: '#/dashboard/playback/transcoding', name: globalize.translate('Transcoding') }, { - href: '#/playbackconfiguration.html', + href: '#/dashboard/playback/resume', name: globalize.translate('ButtonResume') }, { - href: '#/streamingsettings.html', + href: '#/dashboard/playback/streaming', name: globalize.translate('TabStreaming') }]; } diff --git a/src/controllers/dashboard/library.js b/src/controllers/dashboard/library.js index 51ca3af712..62d9329969 100644 --- a/src/controllers/dashboard/library.js +++ b/src/controllers/dashboard/library.js @@ -360,16 +360,16 @@ function getVirtualFolderHtml(page, virtualFolder, index) { function getTabs() { return [{ - href: '#/library.html', + href: '#/dashboard/libraries', name: globalize.translate('HeaderLibraries') }, { - href: '#/librarydisplay.html', + href: '#/dashboard/libraries/display', name: globalize.translate('Display') }, { - href: '#/metadataimages.html', + href: '#/dashboard/libraries/metadata', name: globalize.translate('Metadata') }, { - href: '#/metadatanfo.html', + href: '#/dashboard/libraries/nfo', name: globalize.translate('TabNfoSettings') }]; } diff --git a/src/controllers/dashboard/librarydisplay.js b/src/controllers/dashboard/librarydisplay.js index b418984fb7..a38608277d 100644 --- a/src/controllers/dashboard/librarydisplay.js +++ b/src/controllers/dashboard/librarydisplay.js @@ -7,16 +7,16 @@ import Dashboard from '../../utils/dashboard'; function getTabs() { return [{ - href: '#/library.html', + href: '#/dashboard/libraries', name: globalize.translate('HeaderLibraries') }, { - href: '#/librarydisplay.html', + href: '#/dashboard/libraries/display', name: globalize.translate('Display') }, { - href: '#/metadataimages.html', + href: '#/dashboard/libraries/metadata', name: globalize.translate('Metadata') }, { - href: '#/metadatanfo.html', + href: '#/dashboard/libraries/nfo', name: globalize.translate('TabNfoSettings') }]; } diff --git a/src/controllers/dashboard/metadataImages.js b/src/controllers/dashboard/metadataImages.js index 53633d6915..779727dc7a 100644 --- a/src/controllers/dashboard/metadataImages.js +++ b/src/controllers/dashboard/metadataImages.js @@ -88,16 +88,16 @@ function onSubmit() { function getTabs() { return [{ - href: '#/library.html', + href: '#/dashboard/libraries', name: globalize.translate('HeaderLibraries') }, { - href: '#/librarydisplay.html', + href: '#/dashboard/libraries/display', name: globalize.translate('Display') }, { - href: '#/metadataimages.html', + href: '#/dashboard/libraries/metadata', name: globalize.translate('Metadata') }, { - href: '#/metadatanfo.html', + href: '#/dashboard/libraries/nfo', name: globalize.translate('TabNfoSettings') }]; } diff --git a/src/controllers/dashboard/metadatanfo.js b/src/controllers/dashboard/metadatanfo.js index 38e2ec71fe..1a68d4d63c 100644 --- a/src/controllers/dashboard/metadatanfo.js +++ b/src/controllers/dashboard/metadatanfo.js @@ -46,16 +46,16 @@ function showConfirmMessage() { function getTabs() { return [{ - href: '#/library.html', + href: '#/dashboard/libraries', name: globalize.translate('HeaderLibraries') }, { - href: '#/librarydisplay.html', + href: '#/dashboard/libraries/display', name: globalize.translate('Display') }, { - href: '#/metadataimages.html', + href: '#/dashboard/libraries/metadata', name: globalize.translate('Metadata') }, { - href: '#/metadatanfo.html', + href: '#/dashboard/libraries/nfo', name: globalize.translate('TabNfoSettings') }]; } diff --git a/src/controllers/dashboard/playback.js b/src/controllers/dashboard/playback.js index f24e77efd9..6b4985df34 100644 --- a/src/controllers/dashboard/playback.js +++ b/src/controllers/dashboard/playback.js @@ -31,13 +31,13 @@ function onSubmit() { function getTabs() { return [{ - href: '#/encodingsettings.html', + href: '#/dashboard/playback/transcoding', name: globalize.translate('Transcoding') }, { - href: '#/playbackconfiguration.html', + href: '#/dashboard/playback/resume', name: globalize.translate('ButtonResume') }, { - href: '#/streamingsettings.html', + href: '#/dashboard/playback/streaming', name: globalize.translate('TabStreaming') }]; } diff --git a/src/controllers/dashboard/plugins/available/index.js b/src/controllers/dashboard/plugins/available/index.js index 78368b2c14..b3445b5cb7 100644 --- a/src/controllers/dashboard/plugins/available/index.js +++ b/src/controllers/dashboard/plugins/available/index.js @@ -120,7 +120,7 @@ function onSearchBarType(searchBar) { function getPluginHtml(plugin, options, installedPlugins) { let html = ''; - let href = plugin.externalUrl ? plugin.externalUrl : '#/addplugin.html?name=' + encodeURIComponent(plugin.name) + '&guid=' + plugin.guid; + let href = plugin.externalUrl ? plugin.externalUrl : '#/dashboard/plugins/add?name=' + encodeURIComponent(plugin.name) + '&guid=' + plugin.guid; if (options.context) { href += '&context=' + options.context; @@ -161,13 +161,13 @@ function getPluginHtml(plugin, options, installedPlugins) { function getTabs() { return [{ - href: '#/installedplugins.html', + href: '#/dashboard/plugins', name: globalize.translate('TabMyPlugins') }, { - href: '#/availableplugins.html', + href: '#/dashboard/plugins/catalog', name: globalize.translate('TabCatalog') }, { - href: '#/repositories.html', + href: '#/dashboard/plugins/repositories', name: globalize.translate('TabRepositories') }]; } diff --git a/src/controllers/dashboard/plugins/installed/index.js b/src/controllers/dashboard/plugins/installed/index.js index f6442710fc..91ebcfdff1 100644 --- a/src/controllers/dashboard/plugins/installed/index.js +++ b/src/controllers/dashboard/plugins/installed/index.js @@ -130,7 +130,7 @@ function populateList(page, plugins, pluginConfigurationPages) { } else { html += '
'; html += '

' + globalize.translate('MessageNoPluginsInstalled') + '

'; - html += '

'; + html += '

'; html += globalize.translate('MessageBrowsePluginCatalog'); html += '

'; html += '
'; @@ -221,13 +221,13 @@ function reloadList(page) { function getTabs() { return [{ - href: '#/installedplugins.html', + href: '#/dashboard/plugins', name: globalize.translate('TabMyPlugins') }, { - href: '#/availableplugins.html', + href: '#/dashboard/plugins/catalog', name: globalize.translate('TabCatalog') }, { - href: '#/repositories.html', + href: '#/dashboard/plugins/repositories', name: globalize.translate('TabRepositories') }]; } diff --git a/src/controllers/dashboard/plugins/repositories/index.js b/src/controllers/dashboard/plugins/repositories/index.js index 3c83b6e394..55ff12a456 100644 --- a/src/controllers/dashboard/plugins/repositories/index.js +++ b/src/controllers/dashboard/plugins/repositories/index.js @@ -105,13 +105,13 @@ function getRepositoryElement(repository) { function getTabs() { return [{ - href: '#/installedplugins.html', + href: '#/dashboard/plugins', name: globalize.translate('TabMyPlugins') }, { - href: '#/availableplugins.html', + href: '#/dashboard/plugins/catalog', name: globalize.translate('TabCatalog') }, { - href: '#/repositories.html', + href: '#/dashboard/plugins/repositories', name: globalize.translate('TabRepositories') }]; } diff --git a/src/controllers/dashboard/scheduledtasks/scheduledtasks.js b/src/controllers/dashboard/scheduledtasks/scheduledtasks.js index f17bc5c935..53966e18d1 100644 --- a/src/controllers/dashboard/scheduledtasks/scheduledtasks.js +++ b/src/controllers/dashboard/scheduledtasks/scheduledtasks.js @@ -53,12 +53,12 @@ function populateList(page, tasks) { html += '
'; } html += '
'; - html += ""; + html += ""; html += ''; html += ''; html += '
'; const textAlignStyle = globalize.getIsRTL() ? 'right' : 'left'; - html += ""; + html += ""; html += "

" + task.Name + '

'; html += "
" + getTaskProgressHtml(task) + '
'; html += '
'; diff --git a/src/controllers/dashboard/serveractivity.html b/src/controllers/dashboard/serveractivity.html deleted file mode 100644 index 29cacd300e..0000000000 --- a/src/controllers/dashboard/serveractivity.html +++ /dev/null @@ -1,12 +0,0 @@ -
-
-
-
-

-
-
-
-
-
-
-
diff --git a/src/controllers/dashboard/serveractivity.js b/src/controllers/dashboard/serveractivity.js deleted file mode 100644 index 72e84a89d5..0000000000 --- a/src/controllers/dashboard/serveractivity.js +++ /dev/null @@ -1,32 +0,0 @@ -import ActivityLog from '../../components/activitylog'; -import globalize from '../../scripts/globalize'; -import { toBoolean } from '../../utils/string.ts'; - -export default function (view, params) { - let activityLog; - - if (toBoolean(params.useractivity, true)) { - view.querySelector('.activityItems').setAttribute('data-useractivity', 'true'); - view.querySelector('.sectionTitle').innerHTML = globalize.translate('HeaderActivity'); - } else { - view.querySelector('.activityItems').setAttribute('data-useractivity', 'false'); - view.querySelector('.sectionTitle').innerHTML = globalize.translate('Alerts'); - } - - view.addEventListener('viewshow', function () { - if (!activityLog) { - activityLog = new ActivityLog({ - serverId: ApiClient.serverId(), - element: view.querySelector('.activityItems') - }); - } - }); - view.addEventListener('viewdestroy', function () { - if (activityLog) { - activityLog.destroy(); - } - - activityLog = null; - }); -} - diff --git a/src/controllers/dashboard/streaming.js b/src/controllers/dashboard/streaming.js index ba9d767517..c02a5cdbde 100644 --- a/src/controllers/dashboard/streaming.js +++ b/src/controllers/dashboard/streaming.js @@ -22,13 +22,13 @@ function onSubmit() { function getTabs() { return [{ - href: '#/encodingsettings.html', + href: '#/dashboard/playback/transcoding', name: globalize.translate('Transcoding') }, { - href: '#/playbackconfiguration.html', + href: '#/dashboard/playback/resume', name: globalize.translate('ButtonResume') }, { - href: '#/streamingsettings.html', + href: '#/dashboard/playback/streaming', name: globalize.translate('TabStreaming') }]; } diff --git a/src/controllers/favorites.js b/src/controllers/favorites.js index f14727a764..50c53d90b7 100644 --- a/src/controllers/favorites.js +++ b/src/controllers/favorites.js @@ -1,35 +1,25 @@ -import { appRouter } from '../components/router/appRouter'; -import cardBuilder from '../components/cardbuilder/cardBuilder'; -import dom from '../scripts/dom'; -import globalize from '../scripts/globalize'; -import { appHost } from '../components/apphost'; -import layoutManager from '../components/layoutManager'; -import focusManager from '../components/focusManager'; -import '../elements/emby-itemscontainer/emby-itemscontainer'; -import '../elements/emby-scroller/emby-scroller'; -import ServerConnections from '../components/ServerConnections'; +import { appHost } from 'components/apphost'; +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import focusManager from 'components/focusManager'; +import layoutManager from 'components/layoutManager'; +import { appRouter } from 'components/router/appRouter'; +import ServerConnections from 'components/ServerConnections'; +import dom from 'scripts/dom'; +import globalize from 'scripts/globalize'; +import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card'; + +import 'elements/emby-itemscontainer/emby-itemscontainer'; +import 'elements/emby-scroller/emby-scroller'; function enableScrollX() { return true; } -function getThumbShape() { - return enableScrollX() ? 'overflowBackdrop' : 'backdrop'; -} - -function getPosterShape() { - return enableScrollX() ? 'overflowPortrait' : 'portrait'; -} - -function getSquareShape() { - return enableScrollX() ? 'overflowSquare' : 'square'; -} - function getSections() { return [{ name: 'Movies', types: 'Movie', - shape: getPosterShape(), + shape: getPortraitShape(enableScrollX()), showTitle: true, showYear: true, overlayPlayButton: true, @@ -38,7 +28,7 @@ function getSections() { }, { name: 'Shows', types: 'Series', - shape: getPosterShape(), + shape: getPortraitShape(enableScrollX()), showTitle: true, showYear: true, overlayPlayButton: true, @@ -47,7 +37,7 @@ function getSections() { }, { name: 'Episodes', types: 'Episode', - shape: getThumbShape(), + shape: getBackdropShape(enableScrollX()), preferThumb: false, showTitle: true, showParentTitle: true, @@ -57,7 +47,7 @@ function getSections() { }, { name: 'Videos', types: 'Video', - shape: getThumbShape(), + shape: getBackdropShape(enableScrollX()), preferThumb: true, showTitle: true, overlayPlayButton: true, @@ -66,7 +56,7 @@ function getSections() { }, { name: 'Collections', types: 'BoxSet', - shape: getPosterShape(), + shape: getPortraitShape(enableScrollX()), showTitle: true, overlayPlayButton: true, overlayText: false, @@ -74,7 +64,7 @@ function getSections() { }, { name: 'Playlists', types: 'Playlist', - shape: getSquareShape(), + shape: getSquareShape(enableScrollX()), preferThumb: false, showTitle: true, overlayText: false, @@ -85,7 +75,7 @@ function getSections() { }, { name: 'People', types: 'Person', - shape: getPosterShape(), + shape: getPortraitShape(enableScrollX()), preferThumb: false, showTitle: true, overlayText: false, @@ -96,7 +86,7 @@ function getSections() { }, { name: 'Artists', types: 'MusicArtist', - shape: getSquareShape(), + shape: getSquareShape(enableScrollX()), preferThumb: false, showTitle: true, overlayText: false, @@ -107,7 +97,7 @@ function getSections() { }, { name: 'Albums', types: 'MusicAlbum', - shape: getSquareShape(), + shape: getSquareShape(enableScrollX()), preferThumb: false, showTitle: true, overlayText: false, @@ -118,7 +108,7 @@ function getSections() { }, { name: 'Songs', types: 'Audio', - shape: getSquareShape(), + shape: getSquareShape(enableScrollX()), preferThumb: false, showTitle: true, overlayText: false, @@ -130,7 +120,7 @@ function getSections() { }, { name: 'Books', types: 'Book', - shape: getPosterShape(), + shape: getPortraitShape(enableScrollX()), showTitle: true, showYear: true, overlayPlayButton: true, diff --git a/src/controllers/itemDetails/index.js b/src/controllers/itemDetails/index.js index 8b000348e0..4efe69e781 100644 --- a/src/controllers/itemDetails/index.js +++ b/src/controllers/itemDetails/index.js @@ -4,39 +4,42 @@ import { marked } from 'marked'; import escapeHtml from 'escape-html'; import isEqual from 'lodash-es/isEqual'; -import { appHost } from '../../components/apphost'; -import loading from '../../components/loading/loading'; -import { appRouter } from '../../components/router/appRouter'; -import layoutManager from '../../components/layoutManager'; -import Events from '../../utils/events.ts'; -import * as userSettings from '../../scripts/settings/userSettings'; -import cardBuilder from '../../components/cardbuilder/cardBuilder'; -import datetime from '../../scripts/datetime'; -import mediaInfo from '../../components/mediainfo/mediainfo'; -import { clearBackdrop, setBackdrops } from '../../components/backdrop/backdrop'; -import listView from '../../components/listview/listview'; -import itemContextMenu from '../../components/itemContextMenu'; -import itemHelper from '../../components/itemHelper'; -import dom from '../../scripts/dom'; -import imageLoader from '../../components/images/imageLoader'; -import libraryMenu from '../../scripts/libraryMenu'; -import globalize from '../../scripts/globalize'; -import browser from '../../scripts/browser'; -import { playbackManager } from '../../components/playback/playbackmanager'; -import '../../styles/scrollstyles.scss'; -import '../../elements/emby-itemscontainer/emby-itemscontainer'; -import '../../elements/emby-checkbox/emby-checkbox'; -import '../../elements/emby-button/emby-button'; -import '../../elements/emby-playstatebutton/emby-playstatebutton'; -import '../../elements/emby-ratingbutton/emby-ratingbutton'; -import '../../elements/emby-scroller/emby-scroller'; -import '../../elements/emby-select/emby-select'; -import itemShortcuts from '../../components/shortcuts'; -import Dashboard from '../../utils/dashboard'; -import ServerConnections from '../../components/ServerConnections'; -import confirm from '../../components/confirm/confirm'; -import { download } from '../../scripts/fileDownloader'; -import { getItemBackdropImageUrl } from '../../utils/jellyfin-apiclient/backdropImage'; +import { appHost } from 'components/apphost'; +import { clearBackdrop, setBackdrops } from 'components/backdrop/backdrop'; +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import confirm from 'components/confirm/confirm'; +import imageLoader from 'components/images/imageLoader'; +import itemContextMenu from 'components/itemContextMenu'; +import itemHelper from 'components/itemHelper'; +import mediaInfo from 'components/mediainfo/mediainfo'; +import layoutManager from 'components/layoutManager'; +import listView from 'components/listview/listview'; +import loading from 'components/loading/loading'; +import { playbackManager } from 'components/playback/playbackmanager'; +import { appRouter } from 'components/router/appRouter'; +import itemShortcuts from 'components/shortcuts'; +import ServerConnections from 'components/ServerConnections'; +import browser from 'scripts/browser'; +import datetime from 'scripts/datetime'; +import dom from 'scripts/dom'; +import { download } from 'scripts/fileDownloader'; +import globalize from 'scripts/globalize'; +import libraryMenu from 'scripts/libraryMenu'; +import * as userSettings from 'scripts/settings/userSettings'; +import { getPortraitShape, getSquareShape } from 'utils/card'; +import Dashboard from 'utils/dashboard'; +import Events from 'utils/events'; +import { getItemBackdropImageUrl } from 'utils/jellyfin-apiclient/backdropImage'; + +import 'elements/emby-itemscontainer/emby-itemscontainer'; +import 'elements/emby-checkbox/emby-checkbox'; +import 'elements/emby-button/emby-button'; +import 'elements/emby-playstatebutton/emby-playstatebutton'; +import 'elements/emby-ratingbutton/emby-ratingbutton'; +import 'elements/emby-scroller/emby-scroller'; +import 'elements/emby-select/emby-select'; + +import 'styles/scrollstyles.scss'; function autoFocus(container) { import('../../components/autoFocuser').then(({ default: autoFocuser }) => { @@ -1069,22 +1072,6 @@ function enableScrollX() { return browser.mobile && window.screen.availWidth <= 1000; } -function getPortraitShape(scrollX) { - if (scrollX == null) { - scrollX = enableScrollX(); - } - - return scrollX ? 'overflowPortrait' : 'portrait'; -} - -function getSquareShape(scrollX) { - if (scrollX == null) { - scrollX = enableScrollX(); - } - - return scrollX ? 'overflowSquare' : 'square'; -} - function renderMoreFromSeason(view, item, apiClient) { const section = view.querySelector('.moreFromSeasonSection'); diff --git a/src/controllers/livetv/livetvrecordings.js b/src/controllers/livetv/livetvrecordings.js index 73afca81d7..be62a110ae 100644 --- a/src/controllers/livetv/livetvrecordings.js +++ b/src/controllers/livetv/livetvrecordings.js @@ -1,10 +1,12 @@ -import loading from '../../components/loading/loading'; -import cardBuilder from '../../components/cardbuilder/cardBuilder'; -import imageLoader from '../../components/images/imageLoader'; -import '../../scripts/livetvcomponents'; -import '../../components/listview/listview.scss'; -import '../../elements/emby-itemscontainer/emby-itemscontainer'; -import Dashboard from '../../utils/dashboard'; +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import imageLoader from 'components/images/imageLoader'; +import loading from 'components/loading/loading'; +import { getBackdropShape } from 'utils/card'; +import Dashboard from 'utils/dashboard'; + +import 'scripts/livetvcomponents'; +import 'components/listview/listview.scss'; +import 'elements/emby-itemscontainer/emby-itemscontainer'; function renderRecordings(elem, recordings, cardOptions, scrollX) { if (!elem) { @@ -32,7 +34,7 @@ function renderRecordings(elem, recordings, cardOptions, scrollX) { recordingItems.innerHTML = cardBuilder.getCardsHtml(Object.assign({ items: recordings, shape: scrollX ? 'autooverflow' : 'auto', - defaultShape: scrollX ? 'overflowBackdrop' : 'backdrop', + defaultShape: getBackdropShape(scrollX), showTitle: true, showParentTitle: true, coverImage: true, diff --git a/src/controllers/livetv/livetvschedule.js b/src/controllers/livetv/livetvschedule.js index 605930e9fd..595daab7a6 100644 --- a/src/controllers/livetv/livetvschedule.js +++ b/src/controllers/livetv/livetvschedule.js @@ -1,11 +1,13 @@ -import layoutManager from '../../components/layoutManager'; -import cardBuilder from '../../components/cardbuilder/cardBuilder'; -import imageLoader from '../../components/images/imageLoader'; -import loading from '../../components/loading/loading'; -import '../../scripts/livetvcomponents'; -import '../../elements/emby-button/emby-button'; -import '../../elements/emby-itemscontainer/emby-itemscontainer'; -import Dashboard from '../../utils/dashboard'; +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import imageLoader from 'components/images/imageLoader'; +import layoutManager from 'components/layoutManager'; +import loading from 'components/loading/loading'; +import { getBackdropShape } from 'utils/card'; +import Dashboard from 'utils/dashboard'; + +import 'elements/emby-button/emby-button'; +import 'elements/emby-itemscontainer/emby-itemscontainer'; +import 'scripts/livetvcomponents'; function enableScrollX() { return !layoutManager.desktop; @@ -50,15 +52,11 @@ function renderRecordings(elem, recordings, cardOptions) { imageLoader.lazyChildren(recordingItems); } -function getBackdropShape() { - return enableScrollX() ? 'overflowBackdrop' : 'backdrop'; -} - function renderActiveRecordings(context, promise) { promise.then(function (result) { renderRecordings(context.querySelector('#activeRecordings'), result.Items, { shape: enableScrollX() ? 'autooverflow' : 'auto', - defaultShape: getBackdropShape(), + defaultShape: getBackdropShape(enableScrollX()), showParentTitle: false, showParentTitleOrTitle: true, showTitle: true, diff --git a/src/controllers/livetv/livetvsuggested.js b/src/controllers/livetv/livetvsuggested.js index d8325f7a2b..b01e754290 100644 --- a/src/controllers/livetv/livetvsuggested.js +++ b/src/controllers/livetv/livetvsuggested.js @@ -1,36 +1,25 @@ -import layoutManager from '../../components/layoutManager'; -import * as userSettings from '../../scripts/settings/userSettings'; -import inputManager from '../../scripts/inputManager'; -import loading from '../../components/loading/loading'; -import globalize from '../../scripts/globalize'; -import * as mainTabsManager from '../../components/maintabsmanager'; -import cardBuilder from '../../components/cardbuilder/cardBuilder'; -import imageLoader from '../../components/images/imageLoader'; -import '../../styles/scrollstyles.scss'; -import '../../elements/emby-itemscontainer/emby-itemscontainer'; -import '../../elements/emby-tabs/emby-tabs'; -import '../../elements/emby-button/emby-button'; -import { LibraryTab } from '../../types/libraryTab.ts'; -import Dashboard from '../../utils/dashboard'; +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import imageLoader from 'components/images/imageLoader'; +import layoutManager from 'components/layoutManager'; +import loading from 'components/loading/loading'; +import * as mainTabsManager from 'components/maintabsmanager'; +import globalize from 'scripts/globalize'; +import inputManager from 'scripts/inputManager'; +import * as userSettings from 'scripts/settings/userSettings'; +import { LibraryTab } from 'types/libraryTab'; +import Dashboard from 'utils/dashboard'; +import { getBackdropShape, getPortraitShape } from 'utils/card'; + +import 'elements/emby-itemscontainer/emby-itemscontainer'; +import 'elements/emby-tabs/emby-tabs'; +import 'elements/emby-button/emby-button'; + +import 'styles/scrollstyles.scss'; function enableScrollX() { return !layoutManager.desktop; } -function getBackdropShape() { - if (enableScrollX()) { - return 'overflowBackdrop'; - } - return 'backdrop'; -} - -function getPortraitShape() { - if (enableScrollX()) { - return 'overflowPortrait'; - } - return 'portrait'; -} - function getLimit() { if (enableScrollX()) { return 12; @@ -96,7 +85,7 @@ function reload(page, enableFullRender) { EnableImageTypes: 'Primary,Thumb' }).then(function (result) { renderItems(page, result.Items, 'upcomingTvMovieItems', null, { - shape: getPortraitShape(), + shape: getPortraitShape(enableScrollX()), preferThumb: null, showParentTitle: false }); @@ -147,7 +136,7 @@ function renderItems(page, items, sectionClass, overlayButton, cardOptions) { preferThumb: 'auto', inheritThumb: false, shape: enableScrollX() ? 'autooverflow' : 'auto', - defaultShape: getBackdropShape(), + defaultShape: getBackdropShape(enableScrollX()), showParentTitle: true, showTitle: true, centerText: true, diff --git a/src/controllers/livetvguideprovider.js b/src/controllers/livetvguideprovider.js index 7a133945f2..e87f2ace33 100644 --- a/src/controllers/livetvguideprovider.js +++ b/src/controllers/livetvguideprovider.js @@ -5,7 +5,7 @@ import { getParameterByName } from '../utils/url.ts'; import Events from '../utils/events.ts'; function onListingsSubmitted() { - Dashboard.navigate('livetvstatus.html'); + Dashboard.navigate('dashboard/livetv'); } function init(page, type, providerId) { diff --git a/src/controllers/livetvstatus.js b/src/controllers/livetvstatus.js index 3c0e304939..8532e8ae2a 100644 --- a/src/controllers/livetvstatus.js +++ b/src/controllers/livetvstatus.js @@ -220,9 +220,9 @@ function getProviderName(providerId) { function getProviderConfigurationUrl(providerId) { switch (providerId.toLowerCase()) { case 'xmltv': - return '#/livetvguideprovider.html?type=xmltv'; + return '#/dashboard/livetv/guide?type=xmltv'; case 'schedulesdirect': - return '#/livetvguideprovider.html?type=schedulesdirect'; + return '#/dashboard/livetv/guide?type=schedulesdirect'; } } @@ -249,7 +249,7 @@ function addProvider(button) { } function addDevice() { - Dashboard.navigate('livetvtuner.html'); + Dashboard.navigate('dashboard/livetv/tuner'); } function showDeviceMenu(button, tunerDeviceId) { @@ -274,7 +274,7 @@ function showDeviceMenu(button, tunerDeviceId) { break; case 'edit': - Dashboard.navigate('livetvtuner.html?id=' + tunerDeviceId); + Dashboard.navigate('dashboard/livetv/tuner?id=' + tunerDeviceId); } }); }); @@ -290,7 +290,7 @@ function onDevicesListClick(e) { if (btnCardOptions) { showDeviceMenu(btnCardOptions, id); } else { - Dashboard.navigate('livetvtuner.html?id=' + id); + Dashboard.navigate('dashboard/livetv/tuner?id=' + id); } } } diff --git a/src/controllers/livetvtuner.js b/src/controllers/livetvtuner.js index 7f6ec20270..de73b608d5 100644 --- a/src/controllers/livetvtuner.js +++ b/src/controllers/livetvtuner.js @@ -96,7 +96,7 @@ function submitForm(page) { contentType: 'application/json' }).then(function () { Dashboard.processServerConfigurationUpdateResult(); - Dashboard.navigate('livetvstatus.html'); + Dashboard.navigate('dashboard/livetv'); }, function () { loading.hide(); Dashboard.alert({ diff --git a/src/controllers/movies/moviegenres.js b/src/controllers/movies/moviegenres.js index 3d866c6e9a..36c433c70d 100644 --- a/src/controllers/movies/moviegenres.js +++ b/src/controllers/movies/moviegenres.js @@ -1,12 +1,15 @@ import escapeHtml from 'escape-html'; -import layoutManager from '../../components/layoutManager'; -import loading from '../../components/loading/loading'; -import * as userSettings from '../../scripts/settings/userSettings'; -import cardBuilder from '../../components/cardbuilder/cardBuilder'; -import lazyLoader from '../../components/lazyLoader/lazyLoaderIntersectionObserver'; -import globalize from '../../scripts/globalize'; -import { appRouter } from '../../components/router/appRouter'; -import '../../elements/emby-button/emby-button'; + +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import layoutManager from 'components/layoutManager'; +import lazyLoader from 'components/lazyLoader/lazyLoaderIntersectionObserver'; +import loading from 'components/loading/loading'; +import { appRouter } from 'components/router/appRouter'; +import globalize from 'scripts/globalize'; +import * as userSettings from 'scripts/settings/userSettings'; +import { getBackdropShape, getPortraitShape } from 'utils/card'; + +import 'elements/emby-button/emby-button'; export default function (view, params, tabContent) { function getPageData() { @@ -49,14 +52,6 @@ export default function (view, params, tabContent) { return !layoutManager.desktop; } - function getThumbShape() { - return enableScrollX() ? 'overflowBackdrop' : 'backdrop'; - } - - function getPortraitShape() { - return enableScrollX() ? 'overflowPortrait' : 'portrait'; - } - const fillItemsContainer = (entry) => { const elem = entry.target; const id = elem.getAttribute('data-id'); @@ -85,7 +80,7 @@ export default function (view, params, tabContent) { if (viewStyle == 'Thumb') { cardBuilder.buildCards(result.Items, { itemsContainer: elem, - shape: getThumbShape(), + shape: getBackdropShape(enableScrollX()), preferThumb: true, showTitle: true, scalable: true, @@ -96,7 +91,7 @@ export default function (view, params, tabContent) { } else if (viewStyle == 'ThumbCard') { cardBuilder.buildCards(result.Items, { itemsContainer: elem, - shape: getThumbShape(), + shape: getBackdropShape(enableScrollX()), preferThumb: true, showTitle: true, scalable: true, @@ -107,7 +102,7 @@ export default function (view, params, tabContent) { } else if (viewStyle == 'PosterCard') { cardBuilder.buildCards(result.Items, { itemsContainer: elem, - shape: getPortraitShape(), + shape: getPortraitShape(enableScrollX()), showTitle: true, scalable: true, centerText: false, @@ -117,7 +112,7 @@ export default function (view, params, tabContent) { } else if (viewStyle == 'Poster') { cardBuilder.buildCards(result.Items, { itemsContainer: elem, - shape: getPortraitShape(), + shape: getPortraitShape(enableScrollX()), scalable: true, overlayMoreButton: true, allowBottomPadding: true, diff --git a/src/controllers/movies/moviesrecommended.js b/src/controllers/movies/moviesrecommended.js index 1dc60c30e1..ea593f805d 100644 --- a/src/controllers/movies/moviesrecommended.js +++ b/src/controllers/movies/moviesrecommended.js @@ -1,35 +1,29 @@ import escapeHtml from 'escape-html'; -import layoutManager from '../../components/layoutManager'; -import inputManager from '../../scripts/inputManager'; -import * as userSettings from '../../scripts/settings/userSettings'; -import libraryMenu from '../../scripts/libraryMenu'; -import * as mainTabsManager from '../../components/maintabsmanager'; -import cardBuilder from '../../components/cardbuilder/cardBuilder'; -import dom from '../../scripts/dom'; -import imageLoader from '../../components/images/imageLoader'; -import { playbackManager } from '../../components/playback/playbackmanager'; -import globalize from '../../scripts/globalize'; -import { LibraryTab } from '../../types/libraryTab.ts'; -import Dashboard from '../../utils/dashboard'; -import Events from '../../utils/events.ts'; -import '../../elements/emby-scroller/emby-scroller'; -import '../../elements/emby-itemscontainer/emby-itemscontainer'; -import '../../elements/emby-tabs/emby-tabs'; -import '../../elements/emby-button/emby-button'; +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import imageLoader from 'components/images/imageLoader'; +import layoutManager from 'components/layoutManager'; +import * as mainTabsManager from 'components/maintabsmanager'; +import { playbackManager } from 'components/playback/playbackmanager'; +import dom from 'scripts/dom'; +import globalize from 'scripts/globalize'; +import inputManager from 'scripts/inputManager'; +import libraryMenu from 'scripts/libraryMenu'; +import * as userSettings from 'scripts/settings/userSettings'; +import { LibraryTab } from 'types/libraryTab'; +import { getBackdropShape, getPortraitShape } from 'utils/card'; +import Dashboard from 'utils/dashboard'; +import Events from 'utils/events'; + +import 'elements/emby-scroller/emby-scroller'; +import 'elements/emby-itemscontainer/emby-itemscontainer'; +import 'elements/emby-tabs/emby-tabs'; +import 'elements/emby-button/emby-button'; function enableScrollX() { return !layoutManager.desktop; } -function getPortraitShape() { - return enableScrollX() ? 'overflowPortrait' : 'portrait'; -} - -function getThumbShape() { - return enableScrollX() ? 'overflowBackdrop' : 'backdrop'; -} - function loadLatest(page, userId, parentId) { const options = { IncludeItemTypes: 'Movie', @@ -45,7 +39,7 @@ function loadLatest(page, userId, parentId) { const container = page.querySelector('#recentlyAddedItems'); cardBuilder.buildCards(items, { itemsContainer: container, - shape: getPortraitShape(), + shape: getPortraitShape(enableScrollX()), scalable: true, overlayPlayButton: true, allowBottomPadding: allowBottomPadding, @@ -87,7 +81,7 @@ function loadResume(page, userId, parentId) { cardBuilder.buildCards(result.Items, { itemsContainer: container, preferThumb: true, - shape: getThumbShape(), + shape: getBackdropShape(enableScrollX()), scalable: true, overlayPlayButton: true, allowBottomPadding: allowBottomPadding, @@ -138,7 +132,7 @@ function getRecommendationHtml(recommendation) { } html += cardBuilder.getCardsHtml(recommendation.Items, { - shape: getPortraitShape(), + shape: getPortraitShape(enableScrollX()), scalable: true, overlayPlayButton: true, allowBottomPadding: allowBottomPadding, diff --git a/src/controllers/music/musicartists.js b/src/controllers/music/musicartists.js index 2d89fd8078..c42bda4c76 100644 --- a/src/controllers/music/musicartists.js +++ b/src/controllers/music/musicartists.js @@ -9,7 +9,7 @@ import Events from '../../utils/events.ts'; import '../../elements/emby-itemscontainer/emby-itemscontainer'; -export default function (view, params, tabContent) { +export default function (view, params, tabContent, options) { function getPageData() { const key = getSavedQueryKey(); let pageData = data[key]; @@ -45,7 +45,7 @@ export default function (view, params, tabContent) { } function getSavedQueryKey() { - return `${params.topParentId}-${this.mode}`; + return `${params.topParentId}-${options.mode}`; } const onViewStyleChange = () => { @@ -67,7 +67,7 @@ export default function (view, params, tabContent) { loading.show(); isLoading = true; const query = getQuery(); - const promise = this.mode == 'albumartists' ? + const promise = options.mode == 'albumartists' ? ApiClient.getAlbumArtists(ApiClient.getCurrentUserId(), query) : ApiClient.getArtists(ApiClient.getCurrentUserId(), query); promise.then((result) => { @@ -169,7 +169,7 @@ export default function (view, params, tabContent) { import('../../components/filterdialog/filterdialog').then(({ default: FilterDialog }) => { const filterDialog = new FilterDialog({ query: getQuery(), - mode: this.mode, + mode: options.mode, serverId: ApiClient.serverId() }); Events.on(filterDialog, 'filterchange', function () { diff --git a/src/controllers/music/musicrecommended.js b/src/controllers/music/musicrecommended.js index dc32aca6f6..6dc14b9b56 100644 --- a/src/controllers/music/musicrecommended.js +++ b/src/controllers/music/musicrecommended.js @@ -1,22 +1,24 @@ -import browser from '../../scripts/browser'; -import layoutManager from '../../components/layoutManager'; -import * as userSettings from '../../scripts/settings/userSettings'; -import inputManager from '../../scripts/inputManager'; -import loading from '../../components/loading/loading'; -import cardBuilder from '../../components/cardbuilder/cardBuilder'; -import dom from '../../scripts/dom'; -import imageLoader from '../../components/images/imageLoader'; -import libraryMenu from '../../scripts/libraryMenu'; -import * as mainTabsManager from '../../components/maintabsmanager'; -import globalize from '../../scripts/globalize'; -import { LibraryTab } from '../../types/libraryTab.ts'; -import Dashboard from '../../utils/dashboard'; +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import imageLoader from 'components/images/imageLoader'; +import layoutManager from 'components/layoutManager'; +import loading from 'components/loading/loading'; +import * as mainTabsManager from 'components/maintabsmanager'; +import browser from 'scripts/browser'; +import dom from 'scripts/dom'; +import globalize from 'scripts/globalize'; +import inputManager from 'scripts/inputManager'; +import libraryMenu from 'scripts/libraryMenu'; +import * as userSettings from 'scripts/settings/userSettings'; +import { LibraryTab } from 'types/libraryTab'; +import Dashboard from 'utils/dashboard'; +import { getSquareShape } from 'utils/card'; -import '../../styles/scrollstyles.scss'; -import '../../elements/emby-itemscontainer/emby-itemscontainer'; -import '../../elements/emby-tabs/emby-tabs'; -import '../../elements/emby-button/emby-button'; -import '../../styles/flexstyles.scss'; +import 'elements/emby-itemscontainer/emby-itemscontainer'; +import 'elements/emby-tabs/emby-tabs'; +import 'elements/emby-button/emby-button'; + +import 'styles/flexstyles.scss'; +import 'styles/scrollstyles.scss'; function itemsPerRow() { const screenWidth = dom.getWindowSize().innerWidth; @@ -40,10 +42,6 @@ function enableScrollX() { return !layoutManager.desktop; } -function getSquareShape() { - return enableScrollX() ? 'overflowSquare' : 'square'; -} - function loadLatest(page, parentId) { loading.show(); const userId = ApiClient.getCurrentUserId(); @@ -62,7 +60,7 @@ function loadLatest(page, parentId) { items: items, showUnplayedIndicator: false, showLatestItemsPopup: false, - shape: getSquareShape(), + shape: getSquareShape(enableScrollX()), showTitle: true, showParentTitle: true, lazy: true, @@ -108,7 +106,7 @@ function loadRecentlyPlayed(page, parentId) { itemsContainer.innerHTML = cardBuilder.getCardsHtml({ items: result.Items, showUnplayedIndicator: false, - shape: getSquareShape(), + shape: getSquareShape(enableScrollX()), showTitle: true, showParentTitle: true, action: 'instantmix', @@ -150,7 +148,7 @@ function loadFrequentlyPlayed(page, parentId) { itemsContainer.innerHTML = cardBuilder.getCardsHtml({ items: result.Items, showUnplayedIndicator: false, - shape: getSquareShape(), + shape: getSquareShape(enableScrollX()), showTitle: true, showParentTitle: true, action: 'instantmix', @@ -260,6 +258,14 @@ export default function (view, params) { mainTabsManager.setTabs(view, currentTabIndex, getTabs, getTabContainers, onBeforeTabChange, onTabChange); } + function getMode(index) { + if (index === 2) { + return 'albumartists'; + } else if (index === 3) { + return 'artists'; + } + } + const getTabController = (page, index, callback) => { let depends; @@ -306,13 +312,9 @@ export default function (view, params) { if (index === 1) { controller = this; } else { - controller = new ControllerFactory(view, params, tabContent); - } - - if (index == 2) { - controller.mode = 'albumartists'; - } else if (index == 3) { - controller.mode = 'artists'; + controller = new ControllerFactory(view, params, tabContent, { + mode: getMode(index) + }); } tabControllers[index] = controller; diff --git a/src/controllers/playback/video/index.js b/src/controllers/playback/video/index.js index 4fd441975b..2cb099856b 100644 --- a/src/controllers/playback/video/index.js +++ b/src/controllers/playback/video/index.js @@ -1,4 +1,5 @@ import escapeHtml from 'escape-html'; +import debounce from 'lodash-es/debounce'; import { playbackManager } from '../../../components/playback/playbackmanager'; import browser from '../../../scripts/browser'; import dom from '../../../scripts/dom'; @@ -258,9 +259,9 @@ export default function (view) { let mouseIsDown = false; - function showOsd() { + function showOsd(focusElement) { slideDownToShow(headerElement); - showMainOsdControls(); + showMainOsdControls(focusElement); resetIdle(); } @@ -313,7 +314,9 @@ export default function (view) { }); } - function showMainOsdControls() { + const _focus = debounce((focusElement) => focusManager.focus(focusElement), 50); + + function showMainOsdControls(focusElement) { if (!currentVisibleMenu) { const elem = osdBottomElement; currentVisibleMenu = 'osd'; @@ -321,12 +324,14 @@ export default function (view) { elem.classList.remove('hide'); elem.classList.remove('videoOsdBottom-hidden'); + focusElement ||= elem.querySelector('.btnPause'); + if (!layoutManager.mobile) { - setTimeout(function () { - focusManager.focus(elem.querySelector('.btnPause')); - }, 50); + _focus(focusElement); } toggleSubtitleSync(); + } else if (currentVisibleMenu === 'osd' && focusElement && !layoutManager.mobile) { + _focus(focusElement); } } @@ -1174,18 +1179,37 @@ export default function (view) { const key = keyboardnavigation.getKeyName(e); const isKeyModified = e.ctrlKey || e.altKey || e.metaKey; + const btnPlayPause = osdBottomElement.querySelector('.btnPause'); + if (e.keyCode === 32) { if (e.target.tagName !== 'BUTTON' || !layoutManager.tv) { playbackManager.playPause(currentPlayer); + showOsd(btnPlayPause); e.preventDefault(); e.stopPropagation(); // Trick Firefox with a null element to skip next click clickedElement = null; + } else { + showOsd(); } - showOsd(); return; } + if (layoutManager.tv && !currentVisibleMenu) { + // Change the behavior of some keys when the OSD is hidden + switch (key) { + case 'ArrowLeft': + case 'ArrowRight': + showOsd(nowPlayingPositionSlider); + nowPlayingPositionSlider.dispatchEvent(new KeyboardEvent(e.type, e)); + return; + case 'Enter': + playbackManager.playPause(currentPlayer); + showOsd(btnPlayPause); + return; + } + } + if (layoutManager.tv && keyboardnavigation.isNavigationKey(key)) { showOsd(); return; @@ -1205,7 +1229,7 @@ export default function (view) { break; case 'k': playbackManager.playPause(currentPlayer); - showOsd(); + showOsd(btnPlayPause); break; case 'ArrowUp': case 'Up': @@ -1219,23 +1243,21 @@ export default function (view) { case 'ArrowRight': case 'Right': playbackManager.fastForward(currentPlayer); - showOsd(); + showOsd(btnFastForward); break; case 'j': case 'ArrowLeft': case 'Left': playbackManager.rewind(currentPlayer); - showOsd(); + showOsd(btnRewind); break; case 'f': if (!e.ctrlKey && !e.metaKey) { playbackManager.toggleFullscreen(currentPlayer); - showOsd(); } break; case 'm': playbackManager.toggleMute(currentPlayer); - showOsd(); break; case 'p': case 'P': @@ -1255,7 +1277,7 @@ export default function (view) { // Ignores gamepad events that are always triggered, even when not focused. if (document.hasFocus()) { /* eslint-disable-line compat/compat */ playbackManager.rewind(currentPlayer); - showOsd(); + showOsd(btnRewind); } break; case 'NavigationRight': @@ -1264,7 +1286,7 @@ export default function (view) { // Ignores gamepad events that are always triggered, even when not focused. if (document.hasFocus()) { /* eslint-disable-line compat/compat */ playbackManager.fastForward(currentPlayer); - showOsd(); + showOsd(btnFastForward); } break; case 'Home': diff --git a/src/controllers/shows/tvgenres.js b/src/controllers/shows/tvgenres.js index 061089af86..3a45be7d5a 100644 --- a/src/controllers/shows/tvgenres.js +++ b/src/controllers/shows/tvgenres.js @@ -1,12 +1,15 @@ import escapeHtml from 'escape-html'; -import layoutManager from '../../components/layoutManager'; -import loading from '../../components/loading/loading'; -import * as userSettings from '../../scripts/settings/userSettings'; -import cardBuilder from '../../components/cardbuilder/cardBuilder'; -import lazyLoader from '../../components/lazyLoader/lazyLoaderIntersectionObserver'; -import globalize from '../../scripts/globalize'; -import { appRouter } from '../../components/router/appRouter'; -import '../../elements/emby-button/emby-button'; + +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import lazyLoader from 'components/lazyLoader/lazyLoaderIntersectionObserver'; +import layoutManager from 'components/layoutManager'; +import loading from 'components/loading/loading'; +import { appRouter } from 'components/router/appRouter'; +import globalize from 'scripts/globalize'; +import * as userSettings from 'scripts/settings/userSettings'; +import { getBackdropShape, getPortraitShape } from 'utils/card'; + +import 'elements/emby-button/emby-button'; export default function (view, params, tabContent) { function getPageData() { @@ -49,14 +52,6 @@ export default function (view, params, tabContent) { return !layoutManager.desktop; } - function getThumbShape() { - return enableScrollX() ? 'overflowBackdrop' : 'backdrop'; - } - - function getPortraitShape() { - return enableScrollX() ? 'overflowPortrait' : 'portrait'; - } - function fillItemsContainer(entry) { const elem = entry.target; const id = elem.getAttribute('data-id'); @@ -85,7 +80,7 @@ export default function (view, params, tabContent) { if (viewStyle == 'Thumb') { cardBuilder.buildCards(result.Items, { itemsContainer: elem, - shape: getThumbShape(), + shape: getBackdropShape(enableScrollX()), preferThumb: true, showTitle: true, scalable: true, @@ -96,7 +91,7 @@ export default function (view, params, tabContent) { } else if (viewStyle == 'ThumbCard') { cardBuilder.buildCards(result.Items, { itemsContainer: elem, - shape: getThumbShape(), + shape: getBackdropShape(enableScrollX()), preferThumb: true, showTitle: true, scalable: true, @@ -107,7 +102,7 @@ export default function (view, params, tabContent) { } else if (viewStyle == 'PosterCard') { cardBuilder.buildCards(result.Items, { itemsContainer: elem, - shape: getPortraitShape(), + shape: getPortraitShape(enableScrollX()), showTitle: true, scalable: true, centerText: false, @@ -117,7 +112,7 @@ export default function (view, params, tabContent) { } else if (viewStyle == 'Poster') { cardBuilder.buildCards(result.Items, { itemsContainer: elem, - shape: getPortraitShape(), + shape: getPortraitShape(enableScrollX()), scalable: true, showTitle: true, centerText: true, diff --git a/src/controllers/shows/tvrecommended.js b/src/controllers/shows/tvrecommended.js index d3673bf6ae..982420bde7 100644 --- a/src/controllers/shows/tvrecommended.js +++ b/src/controllers/shows/tvrecommended.js @@ -1,21 +1,23 @@ +import autoFocuser from 'components/autoFocuser'; +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import layoutManager from 'components/layoutManager'; +import loading from 'components/loading/loading'; +import * as mainTabsManager from 'components/maintabsmanager'; +import { playbackManager } from 'components/playback/playbackmanager'; +import dom from 'scripts/dom'; +import globalize from 'scripts/globalize'; +import inputManager from 'scripts/inputManager'; +import libraryMenu from 'scripts/libraryMenu'; +import * as userSettings from 'scripts/settings/userSettings'; +import { LibraryTab } from 'types/libraryTab'; +import { getBackdropShape } from 'utils/card'; +import Dashboard from 'utils/dashboard'; +import Events from 'utils/events'; -import inputManager from '../../scripts/inputManager'; -import libraryMenu from '../../scripts/libraryMenu'; -import layoutManager from '../../components/layoutManager'; -import loading from '../../components/loading/loading'; -import dom from '../../scripts/dom'; -import * as userSettings from '../../scripts/settings/userSettings'; -import cardBuilder from '../../components/cardbuilder/cardBuilder'; -import { playbackManager } from '../../components/playback/playbackmanager'; -import * as mainTabsManager from '../../components/maintabsmanager'; -import globalize from '../../scripts/globalize'; -import '../../styles/scrollstyles.scss'; -import '../../elements/emby-itemscontainer/emby-itemscontainer'; -import '../../elements/emby-button/emby-button'; -import { LibraryTab } from '../../types/libraryTab.ts'; -import Dashboard from '../../utils/dashboard'; -import Events from '../../utils/events.ts'; -import autoFocuser from '../../components/autoFocuser'; +import 'elements/emby-itemscontainer/emby-itemscontainer'; +import 'elements/emby-button/emby-button'; + +import 'styles/scrollstyles.scss'; function getTabs() { return [{ @@ -119,7 +121,7 @@ function loadResume(view, userId, parentId) { itemsContainer: container, preferThumb: true, inheritThumb: !userSettings.useEpisodeImagesInNextUpAndResume(), - shape: getThumbShape(), + shape: getBackdropShape(enableScrollX()), scalable: true, overlayPlayButton: true, allowBottomPadding: allowBottomPadding, @@ -217,10 +219,6 @@ function enableScrollX() { return !layoutManager.desktop; } -function getThumbShape() { - return enableScrollX() ? 'overflowBackdrop' : 'backdrop'; -} - export default function (view, params) { function onBeforeTabChange(e) { preLoadTab(view, parseInt(e.detail.selectedTabIndex, 10)); diff --git a/src/controllers/shows/tvupcoming.js b/src/controllers/shows/tvupcoming.js index f8b2f31eaa..f3c7d90ec6 100644 --- a/src/controllers/shows/tvupcoming.js +++ b/src/controllers/shows/tvupcoming.js @@ -1,11 +1,14 @@ -import layoutManager from '../../components/layoutManager'; -import loading from '../../components/loading/loading'; -import datetime from '../../scripts/datetime'; -import cardBuilder from '../../components/cardbuilder/cardBuilder'; -import imageLoader from '../../components/images/imageLoader'; -import globalize from '../../scripts/globalize'; -import '../../styles/scrollstyles.scss'; -import '../../elements/emby-itemscontainer/emby-itemscontainer'; +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import imageLoader from 'components/images/imageLoader'; +import layoutManager from 'components/layoutManager'; +import loading from 'components/loading/loading'; +import datetime from 'scripts/datetime'; +import globalize from 'scripts/globalize'; +import { getBackdropShape } from 'utils/card'; + +import 'elements/emby-itemscontainer/emby-itemscontainer'; + +import 'styles/scrollstyles.scss'; function getUpcomingPromise(context, params) { loading.show(); @@ -40,10 +43,6 @@ function enableScrollX() { return !layoutManager.desktop; } -function getThumbShape() { - return enableScrollX() ? 'overflowBackdrop' : 'backdrop'; -} - function renderUpcoming(elem, items) { const groups = []; let currentGroupName = ''; @@ -105,7 +104,7 @@ function renderUpcoming(elem, items) { html += cardBuilder.getCardsHtml({ items: group.items, showLocationTypeIndicator: false, - shape: getThumbShape(), + shape: getBackdropShape(enableScrollX()), showTitle: true, preferThumb: true, lazy: true, diff --git a/src/controllers/user/menu/index.html b/src/controllers/user/menu/index.html index 1c83bd9d68..8fe6326fc2 100644 --- a/src/controllers/user/menu/index.html +++ b/src/controllers/user/menu/index.html @@ -77,7 +77,7 @@

${HeaderAdmin}

- +
@@ -85,7 +85,7 @@
- +
diff --git a/src/controllers/user/menu/index.js b/src/controllers/user/menu/index.js index 7f8ae7ecb4..035d07492b 100644 --- a/src/controllers/user/menu/index.js +++ b/src/controllers/user/menu/index.js @@ -31,7 +31,7 @@ export default function (view, params) { page.querySelector('.lnkHomePreferences').setAttribute('href', '#/mypreferenceshome.html?userId=' + userId); page.querySelector('.lnkPlaybackPreferences').setAttribute('href', '#/mypreferencesplayback.html?userId=' + userId); page.querySelector('.lnkSubtitlePreferences').setAttribute('href', '#/mypreferencessubtitles.html?userId=' + userId); - page.querySelector('.lnkQuickConnectPreferences').setAttribute('href', '#/mypreferencesquickconnect.html?userId=' + userId); + page.querySelector('.lnkQuickConnectPreferences').setAttribute('href', '#/quickconnect?userId=' + userId); page.querySelector('.lnkControlsPreferences').setAttribute('href', '#/mypreferencescontrols.html?userId=' + userId); const supportsClientSettings = appHost.supports('clientsettings'); diff --git a/src/controllers/user/quickConnect/helper.js b/src/controllers/user/quickConnect/helper.js deleted file mode 100644 index 3b306a9f80..0000000000 --- a/src/controllers/user/quickConnect/helper.js +++ /dev/null @@ -1,17 +0,0 @@ -import globalize from '../../../scripts/globalize'; -import toast from '../../../components/toast/toast'; - -export const authorize = (code, userId) => { - const url = ApiClient.getUrl('/QuickConnect/Authorize?Code=' + code + '&UserId=' + userId); - ApiClient.ajax({ - type: 'POST', - url: url - }, true).then(() => { - toast(globalize.translate('QuickConnectAuthorizeSuccess')); - }).catch(() => { - toast(globalize.translate('QuickConnectAuthorizeFail')); - }); - - // prevent bubbling - return false; -}; diff --git a/src/controllers/user/quickConnect/index.html b/src/controllers/user/quickConnect/index.html deleted file mode 100644 index 6e0e0cbf6f..0000000000 --- a/src/controllers/user/quickConnect/index.html +++ /dev/null @@ -1,15 +0,0 @@ -
-
-
-

${QuickConnect}

-
${QuickConnectDescription}
-
-
- -
- -
-
-
diff --git a/src/controllers/user/quickConnect/index.js b/src/controllers/user/quickConnect/index.js deleted file mode 100644 index 2af89cafcb..0000000000 --- a/src/controllers/user/quickConnect/index.js +++ /dev/null @@ -1,25 +0,0 @@ -import { authorize } from './helper'; -import globalize from '../../../scripts/globalize'; -import toast from '../../../components/toast/toast'; - -export default function (view, params) { - const userId = params.userId || ApiClient.getCurrentUserId(); - - view.addEventListener('viewshow', function () { - const codeElement = view.querySelector('#txtQuickConnectCode'); - - view.querySelector('.quickConnectSettingsContainer').addEventListener('submit', (e) => { - e.preventDefault(); - - if (!codeElement.validity.valid) { - toast(globalize.translate('QuickConnectInvalidCode')); - - return; - } - - // Remove spaces from code - const normalizedCode = codeElement.value.replace(/\s/g, ''); - authorize(normalizedCode, userId); - }); - }); -} diff --git a/src/elements/InputElement.tsx b/src/elements/InputElement.tsx index 99b2e8a087..c1cb4863d5 100644 --- a/src/elements/InputElement.tsx +++ b/src/elements/InputElement.tsx @@ -1,32 +1,72 @@ -import React, { FunctionComponent } from 'react'; +import React, { type FC, useCallback, useEffect, useMemo, useRef } from 'react'; + import globalize from '../scripts/globalize'; -const createInputElement = ({ type, id, label, options }: { type?: string, id?: string, label?: string, options?: string }) => ({ +interface CreateInputElementParams { + type?: string + id?: string + label?: string + initialValue?: string + options?: string +} + +const createInputElement = ({ type, id, label, initialValue, options }: CreateInputElementParams) => ({ __html: `` }); -type IProps = { - type?: string; - id?: string; - label?: string; - options?: string -}; +type InputElementProps = { + containerClassName?: string + onChange?: (value: string) => void +} & CreateInputElementParams; + +const InputElement: FC = ({ + containerClassName, + initialValue, + onChange = () => { /* no-op */ }, + type, + id, + label, + options = '' +}) => { + const container = useRef(null); + + // NOTE: We need to memoize the input html because any re-render will break the webcomponent + const inputHtml = useMemo(() => ( + createInputElement({ + type, + id, + label: globalize.translate(label), + initialValue, + options + }) + // eslint-disable-next-line react-hooks/exhaustive-deps + ), []); + + const onInput = useCallback((e: Event) => { + onChange((e.target as HTMLInputElement).value); + }, [ onChange ]); + + useEffect(() => { + const inputElement = container?.current?.querySelector('input'); + inputElement?.addEventListener('input', onInput); + + return () => { + inputElement?.removeEventListener('input', onInput); + }; + }, [ container, onInput ]); -const InputElement: FunctionComponent = ({ type, id, label, options }: IProps) => { return (
); }; diff --git a/src/elements/emby-slider/emby-slider.js b/src/elements/emby-slider/emby-slider.js index afd9490920..ddbe9c596c 100644 --- a/src/elements/emby-slider/emby-slider.js +++ b/src/elements/emby-slider/emby-slider.js @@ -6,6 +6,7 @@ import './emby-slider.scss'; import 'webcomponents.js/webcomponents-lite'; import '../emby-input/emby-input'; import globalize from '../../scripts/globalize'; +import { decimalCount } from '../../utils/number'; const EmbySliderPrototype = Object.create(HTMLInputElement.prototype); @@ -19,6 +20,27 @@ if (Object.getOwnPropertyDescriptor && Object.defineProperty) { } } +/** + * Returns normalized slider step. + * + * @param {HTMLInputElement} range slider itself + * @param {number|undefined} step step + * @returns {number} normalized slider step. + */ +function normalizeSliderStep(range, step) { + if (step > 0) { + return step; + } + + step = parseFloat(range.step); + + if (step > 0) { + return step; + } + + return 1; +} + /** * Returns slider fraction corresponding to client position. * @@ -37,7 +59,7 @@ function mapClientToFraction(range, clientX) { // Snap to step const valueRange = range.max - range.min; if (range.step !== 'any' && valueRange !== 0) { - const step = (range.step || 1) / valueRange; + const step = normalizeSliderStep(range) / valueRange; fraction = Math.round(fraction / step) * step; } @@ -54,13 +76,23 @@ function mapClientToFraction(range, clientX) { function mapFractionToValue(range, fraction) { let value = (range.max - range.min) * fraction; + let decimals = null; + // Snap to step if (range.step !== 'any') { - const step = range.step || 1; + const step = normalizeSliderStep(range); + decimals = decimalCount(step); value = Math.round(value / step) * step; } - value += parseFloat(range.min); + const min = parseFloat(range.min); + + value += min; + + if (decimals != null) { + decimals = Math.max(decimals, decimalCount(min)); + value = parseFloat(value.toFixed(decimals)); + } return Math.min(Math.max(value, range.min), range.max); } @@ -114,12 +146,12 @@ function updateValues(isValueSet) { }); } -function updateBubble(range, value, bubble) { +function updateBubble(range, percent, value, bubble) { requestAnimationFrame(function () { const bubbleTrackRect = range.sliderBubbleTrack.getBoundingClientRect(); const bubbleRect = bubble.getBoundingClientRect(); - let bubblePos = bubbleTrackRect.width * value / 100; + let bubblePos = bubbleTrackRect.width * percent / 100; if (globalize.getIsElementRTL(range)) { bubblePos = bubbleTrackRect.width - bubblePos; } @@ -127,18 +159,20 @@ function updateBubble(range, value, bubble) { bubble.style.left = bubblePos + 'px'; + let html; + if (range.getBubbleHtml) { - value = range.getBubbleHtml(value); + html = range.getBubbleHtml(percent, value); } else { if (range.getBubbleText) { - value = range.getBubbleText(value); + html = range.getBubbleText(percent, value); } else { - value = mapFractionToValue(range, value / 100).toLocaleString(); + html = value.toLocaleString(); } - value = '

' + value + '

'; + html = '

' + html + '

'; } - bubble.innerHTML = value; + bubble.innerHTML = html; }); } @@ -274,8 +308,8 @@ EmbySliderPrototype.attachedCallback = function () { updateValues.call(this); } - const bubbleValue = mapValueToFraction(this, this.value) * 100; - updateBubble(this, bubbleValue, sliderBubble); + const percent = mapValueToFraction(this, this.value) * 100; + updateBubble(this, percent, parseFloat(this.value), sliderBubble); if (hasHideBubbleClass) { sliderBubble.classList.remove('hide'); @@ -301,9 +335,11 @@ EmbySliderPrototype.attachedCallback = function () { /* eslint-disable-next-line compat/compat */ dom.addEventListener(this, (window.PointerEvent ? 'pointermove' : 'mousemove'), function (e) { if (!this.dragging) { - const bubbleValue = mapClientToFraction(this, e.clientX) * 100; + const fraction = mapClientToFraction(this, e.clientX); + const percent = fraction * 100; + const value = mapFractionToValue(this, fraction); - updateBubble(this, bubbleValue, sliderBubble); + updateBubble(this, percent, value, sliderBubble); if (hasHideBubbleClass) { sliderBubble.classList.remove('hide'); @@ -455,16 +491,23 @@ function onKeyDown(e) { switch (keyboardnavigation.getKeyName(e)) { case 'ArrowLeft': case 'Left': - stepKeyboard(this, -this.keyboardStepDown || -1); + stepKeyboard(this, -normalizeSliderStep(this, this.keyboardStepDown)); e.preventDefault(); e.stopPropagation(); break; case 'ArrowRight': case 'Right': - stepKeyboard(this, this.keyboardStepUp || 1); + stepKeyboard(this, normalizeSliderStep(this, this.keyboardStepUp)); e.preventDefault(); e.stopPropagation(); break; + case 'Enter': + if (this.keyboardDragging) { + finishKeyboardDragging(this); + e.preventDefault(); + e.stopPropagation(); + } + break; } } diff --git a/src/hooks/useSyncPlayGroups.ts b/src/hooks/useSyncPlayGroups.ts new file mode 100644 index 0000000000..e102fab88e --- /dev/null +++ b/src/hooks/useSyncPlayGroups.ts @@ -0,0 +1,24 @@ +import { useQuery } from '@tanstack/react-query'; +import { JellyfinApiContext, useApi } from './useApi'; +import { getSyncPlayApi } from '@jellyfin/sdk/lib/utils/api/sync-play-api'; +import { AxiosRequestConfig } from 'axios'; + +const fetchSyncPlayGroups = async ( + currentApi: JellyfinApiContext, + options?: AxiosRequestConfig +) => { + const { api } = currentApi; + if (!api) throw new Error('No API instance available'); + + const response = await getSyncPlayApi(api) + .syncPlayGetGroups(options); + return response.data; +}; + +export const useSyncPlayGroups = () => { + const currentApi = useApi(); + return useQuery({ + queryKey: [ 'SyncPlay', 'Groups' ], + queryFn: ({ signal }) => fetchSyncPlayGroups(currentApi, { signal }) + }); +}; diff --git a/src/index.jsx b/src/index.jsx index 16f6109314..cf0e6cafb9 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -32,6 +32,7 @@ import './components/playback/playerSelectionMenu'; import './legacy/domParserTextHtml'; import './legacy/focusPreventScroll'; import './legacy/htmlMediaElement'; +import './legacy/keyboardEvent'; import './legacy/vendorStyles'; import { currentSettings } from './scripts/settings/userSettings'; import taskButton from './scripts/taskbutton'; diff --git a/src/legacy/keyboardEvent.js b/src/legacy/keyboardEvent.js new file mode 100644 index 0000000000..8b40e617d0 --- /dev/null +++ b/src/legacy/keyboardEvent.js @@ -0,0 +1,48 @@ +/** + * Polyfill for KeyboardEvent + * - Constructor. + */ + +(function (window) { + 'use strict'; + + try { + new window.KeyboardEvent('event', { bubbles: true, cancelable: true }); + } catch (e) { + // We can't use `KeyboardEvent` in old WebKit because `initKeyboardEvent` + // doesn't seem to populate some properties (`keyCode`, `which`) that + // are read-only. + const KeyboardEventOriginal = window.Event; + + const KeyboardEvent = function (eventName, options) { + options = options || {}; + + const event = document.createEvent('Event'); + + event.initEvent(eventName, !!options.bubbles, !!options.cancelable); + + event.view = options.view || document.defaultView; + + event.key = options.key || options.keyIdentifier || ''; + event.keyCode = options.keyCode || 0; + event.code = options.code || ''; + event.charCode = options.charCode || 0; + event.char = options.char || ''; + event.which = options.which || 0; + + event.location = options.location || options.keyLocation || 0; + + event.ctrlKey = !!options.ctrlKey; + event.altKey = !!options.altKey; + event.shiftKey = !!options.shiftKey; + event.metaKey = !!options.metaKey; + + event.repeat = !!options.repeat; + + return event; + }; + + KeyboardEvent.prototype = KeyboardEventOriginal.prototype; + window.KeyboardEvent = KeyboardEvent; + } +}(window)); diff --git a/src/plugins/backdropScreensaver/plugin.js b/src/plugins/backdropScreensaver/plugin.js index 5f5f440c1b..60d28a67cc 100644 --- a/src/plugins/backdropScreensaver/plugin.js +++ b/src/plugins/backdropScreensaver/plugin.js @@ -5,7 +5,7 @@ import * as userSettings from '../../scripts/settings/userSettings'; class BackdropScreensaver { constructor() { - this.name = 'Backdrop ScreenSaver'; + this.name = 'BackdropScreensaver'; this.type = PluginType.Screensaver; this.id = 'backdropscreensaver'; this.supportsAnonymous = false; diff --git a/src/plugins/chromecastPlayer/plugin.js b/src/plugins/chromecastPlayer/plugin.js index e5663c2f08..c4c90c15bf 100644 --- a/src/plugins/chromecastPlayer/plugin.js +++ b/src/plugins/chromecastPlayer/plugin.js @@ -106,10 +106,9 @@ class CastPlayer { return; } - let applicationID = applicationStable; - if (userSettings.chromecastVersion() === 'unstable') { - applicationID = applicationUnstable; - } + let applicationID = userSettings.chromecastVersion(); + if (applicationID === 'stable') applicationID = applicationStable; + if (applicationID === 'unstable') applicationID = applicationUnstable; // request session const sessionRequest = new chrome.cast.SessionRequest(applicationID); @@ -117,7 +116,7 @@ class CastPlayer { this.sessionListener.bind(this), this.receiverListener.bind(this)); - console.debug('chromecast.initialize'); + console.debug(`chromecast.initialize (applicationId=${applicationID})`); chrome.cast.initialize(apiConfig, this.onInitSuccess.bind(this), this.errorHandler); } diff --git a/src/plugins/htmlVideoPlayer/plugin.js b/src/plugins/htmlVideoPlayer/plugin.js index 194678b7ea..5f20eb116b 100644 --- a/src/plugins/htmlVideoPlayer/plugin.js +++ b/src/plugins/htmlVideoPlayer/plugin.js @@ -1978,6 +1978,18 @@ export class HtmlVideoPlayer { }, { name: '2x', id: 2.0 + }, { + name: '2.5x', + id: 2.5 + }, { + name: '3x', + id: 3.0 + }, { + name: '3.5x', + id: 3.5 + }, { + name: '4.0x', + id: 4.0 }]; } diff --git a/src/plugins/logoScreensaver/plugin.js b/src/plugins/logoScreensaver/plugin.js index 9cd0e1a445..06d643d777 100644 --- a/src/plugins/logoScreensaver/plugin.js +++ b/src/plugins/logoScreensaver/plugin.js @@ -4,7 +4,7 @@ import { randomInt } from '../../utils/number.ts'; export default function () { const self = this; - self.name = 'Logo ScreenSaver'; + self.name = 'LogoScreensaver'; self.type = PluginType.Screensaver; self.id = 'logoscreensaver'; self.supportsAnonymous = true; diff --git a/src/scripts/browserDeviceProfile.js b/src/scripts/browserDeviceProfile.js index c00e188d4b..f6289522f3 100644 --- a/src/scripts/browserDeviceProfile.js +++ b/src/scripts/browserDeviceProfile.js @@ -188,6 +188,27 @@ function supportsVc1(videoTestElement) { return browser.tizen || browser.web0s || browser.edgeUwp || videoTestElement.canPlayType('video/mp4; codecs="vc-1"').replace(/no/, ''); } +function supportsHdr10(options) { + return options.supportsHdr10 ?? (false // eslint-disable-line sonarjs/no-redundant-boolean + || browser.tizen + || browser.web0s + || browser.safari && ((browser.iOS && browser.iOSVersion >= 11) || browser.osx) + // Chrome mobile and Firefox have no client side tone-mapping + // Edge Chromium on Nvidia is known to have color issues on 10-bit video + || browser.chrome && !browser.mobile + ); +} + +function supportsHlg(options) { + return options.supportsHlg ?? supportsHdr10(options); +} + +function supportsDolbyVision(options) { + return options.supportsDolbyVision ?? (false // eslint-disable-line sonarjs/no-redundant-boolean + || browser.safari && ((browser.iOS && browser.iOSVersion >= 13) || browser.osx) + ); +} + function getDirectPlayProfileForVideoContainer(container, videoAudioCodecs, videoTestElement, options) { let supported = false; let profileContainer = container; @@ -897,25 +918,20 @@ export default function (options) { let vp9VideoRangeTypes = 'SDR'; let av1VideoRangeTypes = 'SDR'; - if (browser.safari && ((browser.iOS && browser.iOSVersion >= 11) || browser.osx)) { - hevcVideoRangeTypes += '|HDR10|HLG'; - if ((browser.iOS && browser.iOSVersion >= 13) || browser.osx) { - hevcVideoRangeTypes += '|DOVI'; - } + if (supportsHdr10(options)) { + hevcVideoRangeTypes += '|HDR10'; + vp9VideoRangeTypes += '|HDR10'; + av1VideoRangeTypes += '|HDR10'; } - if (browser.tizen || browser.web0s) { - hevcVideoRangeTypes += '|HDR10|HLG'; - vp9VideoRangeTypes += '|HDR10|HLG'; - av1VideoRangeTypes += '|HDR10|HLG'; + if (supportsHlg(options)) { + hevcVideoRangeTypes += '|HLG'; + vp9VideoRangeTypes += '|HLG'; + av1VideoRangeTypes += '|HLG'; } - // Chrome mobile and Firefox have no client side tone-mapping - // Edge Chromium on Nvidia is known to have color issues on 10-bit video - if (browser.chrome && !browser.mobile) { - hevcVideoRangeTypes += '|HDR10|HLG'; - vp9VideoRangeTypes += '|HDR10|HLG'; - av1VideoRangeTypes += '|HDR10|HLG'; + if (supportsDolbyVision(options)) { + hevcVideoRangeTypes += '|DOVI'; } const h264CodecProfileConditions = [ diff --git a/src/scripts/libraryMenu.js b/src/scripts/libraryMenu.js index 0f3b4a73e1..f90ece9d79 100644 --- a/src/scripts/libraryMenu.js +++ b/src/scripts/libraryMenu.js @@ -327,8 +327,8 @@ function refreshLibraryInfoInDrawer(user) { html += '

'; html += globalize.translate('HeaderAdmin'); html += '

'; - html += `
${globalize.translate('TabDashboard')}`; - html += `${globalize.translate('Metadata')}`; + html += `${globalize.translate('TabDashboard')}`; + html += `${globalize.translate('Metadata')}`; html += '
'; } @@ -376,249 +376,6 @@ function refreshLibraryInfoInDrawer(user) { } } -function refreshDashboardInfoInDrawer(page, apiClient) { - currentDrawerType = 'admin'; - loadNavDrawer(); - - if (navDrawerScrollContainer.querySelector('.adminDrawerLogo')) { - updateDashboardMenuSelectedItem(page); - } else { - createDashboardMenu(page, apiClient); - } -} - -function isUrlInCurrentView(url) { - return window.location.href.toString().toLowerCase().indexOf(url.toLowerCase()) !== -1; -} - -function updateDashboardMenuSelectedItem(page) { - const links = navDrawerScrollContainer.querySelectorAll('.navMenuOption'); - const currentViewId = page.id; - - for (let i = 0, length = links.length; i < length; i++) { - let link = links[i]; - let selected = false; - let pageIds = link.getAttribute('data-pageids'); - - if (pageIds) { - pageIds = pageIds.split('|'); - selected = pageIds.indexOf(currentViewId) != -1; - } - - let pageUrls = link.getAttribute('data-pageurls'); - - if (pageUrls) { - pageUrls = pageUrls.split('|'); - selected = pageUrls.filter(isUrlInCurrentView).length > 0; - } - - if (selected) { - link.classList.add('navMenuOption-selected'); - let title = ''; - link = link.querySelector('.navMenuOptionText') || link; - title += (link.innerText || link.textContent).trim(); - LibraryMenu.setTitle(title); - } else { - link.classList.remove('navMenuOption-selected'); - } - } -} - -function createToolsMenuList(pluginItems) { - const links = [{ - name: globalize.translate('TabServer') - }, { - name: globalize.translate('TabDashboard'), - href: '#/dashboard.html', - pageIds: ['dashboardPage'], - icon: 'dashboard' - }, { - name: globalize.translate('General'), - href: '#/dashboardgeneral.html', - pageIds: ['dashboardGeneralPage'], - icon: 'settings' - }, { - name: globalize.translate('HeaderUsers'), - href: '#/userprofiles.html', - pageIds: ['userProfilesPage', 'newUserPage', 'editUserPage', 'userLibraryAccessPage', 'userParentalControlPage', 'userPasswordPage'], - icon: 'people' - }, { - name: globalize.translate('HeaderLibraries'), - href: '#/library.html', - pageIds: ['mediaLibraryPage', 'librarySettingsPage', 'libraryDisplayPage', 'metadataImagesConfigurationPage', 'metadataNfoPage'], - icon: 'folder' - }, { - name: globalize.translate('TitlePlayback'), - icon: 'play_arrow', - href: '#/encodingsettings.html', - pageIds: ['encodingSettingsPage', 'playbackConfigurationPage', 'streamingSettingsPage'] - }]; - addPluginPagesToMainMenu(links, pluginItems, 'server'); - links.push({ - divider: true, - name: globalize.translate('HeaderDevices') - }); - links.push({ - name: globalize.translate('HeaderDevices'), - href: '#/devices.html', - pageIds: ['devicesPage', 'devicePage'], - icon: 'devices' - }); - links.push({ - name: globalize.translate('HeaderActivity'), - href: '#/dashboard/activity', - pageIds: ['serverActivityPage'], - icon: 'assessment' - }); - links.push({ - name: globalize.translate('DLNA'), - href: '#/dlnasettings.html', - pageIds: ['dlnaSettingsPage', 'dlnaProfilesPage', 'dlnaProfilePage'], - icon: 'input' - }); - links.push({ - divider: true, - name: globalize.translate('LiveTV') - }); - links.push({ - name: globalize.translate('LiveTV'), - href: '#/livetvstatus.html', - pageIds: ['liveTvStatusPage', 'liveTvTunerPage'], - icon: 'live_tv' - }); - links.push({ - name: globalize.translate('HeaderDVR'), - href: '#/livetvsettings.html', - pageIds: ['liveTvSettingsPage'], - icon: 'dvr' - }); - addPluginPagesToMainMenu(links, pluginItems, 'livetv'); - links.push({ - divider: true, - name: globalize.translate('TabAdvanced') - }); - links.push({ - name: globalize.translate('TabNetworking'), - icon: 'cloud', - href: '#/networking.html', - pageIds: ['networkingPage'] - }); - links.push({ - name: globalize.translate('HeaderApiKeys'), - icon: 'vpn_key', - href: '#/apikeys.html', - pageIds: ['apiKeysPage'] - }); - links.push({ - name: globalize.translate('TabLogs'), - href: '#/log.html', - pageIds: ['logPage'], - icon: 'bug_report' - }); - links.push({ - name: globalize.translate('Notifications'), - icon: 'notifications', - href: '#/notificationsettings.html' - }); - links.push({ - name: globalize.translate('TabPlugins'), - icon: 'shopping_cart', - href: '#/installedplugins.html', - pageIds: ['pluginsPage', 'pluginCatalogPage'] - }); - links.push({ - name: globalize.translate('TabScheduledTasks'), - href: '#/scheduledtasks.html', - pageIds: ['scheduledTasksPage', 'scheduledTaskPage'], - icon: 'schedule' - }); - if (hasUnsortedPlugins(pluginItems)) { - links.push({ - divider: true, - name: globalize.translate('TabPlugins') - }); - addPluginPagesToMainMenu(links, pluginItems); - } - return links; -} - -function hasUnsortedPlugins(pluginItems) { - for (const pluginItem of pluginItems) { - if (pluginItem.EnableInMainMenu && pluginItem.MenuSection === undefined) { - return true; - } - } - return false; -} - -function addPluginPagesToMainMenu(links, pluginItems, section) { - for (const pluginItem of pluginItems) { - if (pluginItem.EnableInMainMenu && pluginItem.MenuSection === section) { - links.push({ - name: pluginItem.DisplayName, - icon: pluginItem.MenuIcon || 'folder', - href: Dashboard.getPluginUrl(pluginItem.Name), - pageUrls: [Dashboard.getPluginUrl(pluginItem.Name)] - }); - } - } -} - -function getToolsMenuLinks(apiClient) { - return apiClient.getJSON(apiClient.getUrl('web/configurationpages') + '?pageType=PluginConfiguration&EnableInMainMenu=true').then(createToolsMenuList, function () { - return createToolsMenuList([]); - }); -} - -function getToolsLinkHtml(item) { - let menuHtml = ''; - let pageIds = item.pageIds ? item.pageIds.join('|') : ''; - pageIds = pageIds ? ' data-pageids="' + pageIds + '"' : ''; - let pageUrls = item.pageUrls ? item.pageUrls.join('|') : ''; - pageUrls = pageUrls ? ' data-pageurls="' + pageUrls + '"' : ''; - menuHtml += ''; - - if (item.icon) { - menuHtml += ''; - } - - menuHtml += ''; - menuHtml += escapeHtml(item.name); - menuHtml += ''; - return menuHtml + ''; -} - -function getToolsMenuHtml(apiClient) { - return getToolsMenuLinks(apiClient).then(function (items) { - let menuHtml = ''; - menuHtml += '
'; - - for (const item of items) { - if (item.href) { - menuHtml += getToolsLinkHtml(item); - } else if (item.name) { - menuHtml += '

'; - menuHtml += escapeHtml(item.name); - menuHtml += '

'; - } - } - - return menuHtml + '
'; - }); -} - -function createDashboardMenu(page, apiClient) { - return getToolsMenuHtml(apiClient).then(function (toolsMenuHtml) { - let html = ''; - html += ''; - html += toolsMenuHtml; - navDrawerScrollContainer.innerHTML = html; - updateDashboardMenuSelectedItem(page); - }); -} - function onSidebarLinkClick() { const section = this.getElementsByClassName('sectionName')[0]; const text = section ? section.innerHTML : this.innerHTML; @@ -1026,15 +783,8 @@ pageClassOn('pageshow', 'page', function (e) { const isDashboardPage = page.classList.contains('type-interior'); const isHomePage = page.classList.contains('homePage'); const isLibraryPage = !isDashboardPage && page.classList.contains('libraryPage'); - const apiClient = getCurrentApiClient(); - if (isDashboardPage) { - if (mainDrawerButton) { - mainDrawerButton.classList.remove('hide'); - } - - refreshDashboardInfoInDrawer(page, apiClient); - } else { + if (!isDashboardPage) { if (mainDrawerButton) { if (enableLibraryNavDrawer || (isHomePage && enableLibraryNavDrawerHome)) { mainDrawerButton.classList.remove('hide'); diff --git a/src/scripts/livetvcomponents.js b/src/scripts/livetvcomponents.js index 38035c3dc2..68a2c53453 100644 --- a/src/scripts/livetvcomponents.js +++ b/src/scripts/livetvcomponents.js @@ -1,15 +1,13 @@ -import layoutManager from '../components/layoutManager'; +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import layoutManager from 'components/layoutManager'; +import { getBackdropShape } from 'utils/card'; + import datetime from './datetime'; -import cardBuilder from '../components/cardbuilder/cardBuilder'; function enableScrollX() { return !layoutManager.desktop; } -function getBackdropShape() { - return enableScrollX() ? 'overflowBackdrop' : 'backdrop'; -} - function getTimersHtml(timers, options) { options = options || {}; @@ -78,7 +76,7 @@ function getTimersHtml(timers, options) { html += cardBuilder.getCardsHtml({ items: group.items, - shape: getBackdropShape(), + shape: getBackdropShape(enableScrollX()), showTitle: true, showParentTitleOrTitle: true, showAirTime: true, diff --git a/src/strings/ar.json b/src/strings/ar.json index 5636883825..f86a878922 100644 --- a/src/strings/ar.json +++ b/src/strings/ar.json @@ -524,10 +524,10 @@ "MessageNoPluginsInstalled": "ليس عندك أي ملحقات مثبتة.", "MessageNoTrailersFound": "قم بتثبيت قناة العروض الإعلانية لتحسين متعة المشاهدة بإضافة مكتبة عروض إعلانية من الإنترنت.", "MessageNothingHere": "لا شىء هنا.", - "MessagePasswordResetForUsers": "تم إعادة تعيين كلمات المرور الخاصة بهم للمستخدمين التاليين. يمكنهم الآن تسجيل الدخول باستخدام رموز PIN سهلة الاستخدام التي تم استخدامها لإعادة الضبط.", + "MessagePasswordResetForUsers": "تم إعادة تعيين كلمات المرور للمستخدمين التاليين. يمكنهم الآن تسجيل الدخول باستخدام رموز الـPIN التي تم استخدامها لإعادة الضبط.", "MessagePleaseEnsureInternetMetadata": "الرجاء التأكد من أن إمكانية إنزال واصفات البيانات من الإنترنت ممكنة.", "MessagePluginConfigurationRequiresLocalAccess": "لضبط هذا البرنامج المساعد ، يرجى تسجيل الدخول إلى الخادم المحلي الخاص بك مباشرة.", - "MessagePluginInstallDisclaimer": "إن الملحقات التي بناها أعضاء مجتمع Jellyfin لهي طريقة رائعة لتحسين متعة استخدام Jellyfin وذلك بإضافة المزايا والخدمات الجديدة. قبل تثبيت الملحقات، نرجو أخذ العلم بالآثار التي قد تلحقها بخادم Jellyfin الخاص بك، مثل أوقات أطولة لتمشيط مكتبتك، والعمليات الخلفية الإضافية وتقليل استقرار نظامك.", + "MessagePluginInstallDisclaimer": "تحذير: تنصيب الملحقات التي بناها أعضاء مجتمع Jellyfin هي طريقة رائعة لتحسين متعة استخدام Jellyfin عن طريق أضافة مزايا وخدمات الجديدة. ولكن تنصيب ملحقات من مصادر ثالثة تحمل بعظ الخطورة.\nقبل تثبيت الملحقات، نرجو أخذ العلم بالآثار التي قد تلحقها بخادم Jellyfin الخاص بك، مثل أوقات أطولة لتمشيط مكتبتك، والعمليات الخلفية الإضافية وتقليل استقرار نظامك.", "MessageReenableUser": "أنظر أدناه لإعادة التفعيل", "MessageTheFollowingLocationWillBeRemovedFromLibrary": "مكان الوسائط التالي سيزال من مكتبة Jellyfin الخاصة بك", "MessageUnableToConnectToServer": "لم نستطع الاتصال إلى الخادم المختار في الوقت الحالي. الرجاء التأكد من أنه يعمل ثم المحاولة مرة أخرى.", @@ -1018,7 +1018,7 @@ "Image": "صورة", "Other": "اخري", "EnableQuickConnect": "تفعيل الاتصال السريع على هذا الخادم", - "EnableAutoCast": "تعيين كافتراضي", + "EnableAutoCast": "تعيين كالخيار ألأفتراضي", "Data": "بيانات", "ButtonUseQuickConnect": "استخدم الاتصال السريع", "ButtonActivate": "تفعيل", @@ -1564,7 +1564,7 @@ "LabelAutomaticallyAddToCollection": "إضافة إلى المجموعة تلقائيا", "Console": "وحدة التحكم", "Casual": "غير رسمي", - "AllowTonemappingHelp": "يمكن أن يؤدي تعيين النغمة إلى تحويل النطاق الديناميكي لمقطع فيديو من HDR إلى SDR مع الحفاظ على تفاصيل الصورة والألوان ، وهي معلومات مهمة جدًا لتمثيل المشهد الأصلي. يعمل حاليًا فقط مع مقاطع فيديو HDR10 أو HLG. يتطلب هذا وقت تشغيل OpenCL أو CUDA المقابل.", + "AllowTonemappingHelp": "يمكن أن يؤدي تعيين النغمة إلى تحويل النطاق الديناميكي لمقاطع الفيديو من HDR إلى SDR مع الحفاظ على تفاصيل الصورة والألوان. هذه بينات مهمة جدًا لتمثيل المشهد الأصلي بشكل وفي للمقطع ألأصلي. حاليًا يعمل هذا ألاعداد فقط مع مقاطع فيديو HDR10 أو HLG. يتطلب هذا ألأعداد وقت تشغيل OpenCL أو CUDA.", "RefFramesNotSupported": "الإطارات المرجعية غير مدعومة", "InterlacedVideoNotSupported": "الفيديو المتشابك غير مدعوم", "AnamorphicVideoNotSupported": "لا يتم دعم الفيديو ذي الصورة المشوهة", @@ -1683,8 +1683,8 @@ "Unreleased": "لم يصدر حتى الآن", "MessageRenameMediaFolder": "يرجى ملاحظة أنه ستحذف جميع البيانات الوصفية عند إعادة تسمية مكتبة الوسائط.", "DownloadAll": "تحميل الكل", - "LabelChapterImageResolutionHelp": "دقة الصور المستأصلة من الفصل.", - "LabelDummyChapterDurationHelp": "فترة استئصال الصور من الفصل بالثواني.", + "LabelChapterImageResolutionHelp": "دقة الصور المستأصلة من الفصل. تغيير هذا ألاعداد لن يؤثر على الفصول الوهمية الذي تم أنشائها مسبقاً.", + "LabelDummyChapterDurationHelp": "فترة الوقت بين ألفصول ألوهمية بالثواني. قم بتعيينه ألى 0 لتعطيل ميزة أنشاء الفصول ألوهمية. تغيير هذا ألاعداد لن يؤثر على الفصول الوهمية الذي تم أنشائها مسبقاً.", "Experimental": "تجريبي", "HeaderDummyChapter": "صور الفصل", "HeaderPerformance": "الأداء", @@ -1696,5 +1696,30 @@ "LabelChapterImageResolution": "الدقة", "LabelEnableAudioVbr": "فعل تشفير VBR الصوتي", "AllowCollectionManagement": "السماح لهذا المستخدم بإدارة المجموعات", - "Notifications": "الإشعارات" + "Notifications": "الإشعارات", + "LabelEnableLUFSScan": "تفعيل فحص LUFS", + "LabelEnableLUFSScanHelp": "يمكن للعملاء توسيط ألصوت للحصول على مستوى صوت متساوي عبر المقاطع الصوتية. سيجعل هذا فحص المكتبة أطول ويستهلك مزيدًا من الموارد.", + "LabelMediaDetails": "تفاصيل الميديا", + "LogLevel.Warning": "تحذير", + "LogLevel.Information": "ألمعلومات", + "BackdropScreensaver": "خلفية شاشة التوقف", + "LabelDate": "تاريخ", + "LabelDeveloper": "مُطور", + "LabelBackdropScreensaverIntervalHelp": "ألوقت بالثواني بين الخلفيات المختلفة عند أستخدام خلفية شاشة التوقف.", + "LogLevel.Error": "خطأ", + "LogoScreensaver": "علامة حامية الشاشة", + "MenuClose": "أغلق القائمة", + "LabelSystem": "ألنظام", + "Unknown": "غير معروف", + "LogLevel.Critical": "خطير", + "LogLevel.None": "لا شيئ", + "MenuOpen": "أفتح القائمة", + "AllowSegmentDeletion": "ألغاء القسم", + "AllowSegmentDeletionHelp": "ألغي الأقسام القديمة بعد أن يتم أرسالها للعميل. هذا يساهم بمنع أن يتم تخزين الملف ألذي تم أعادة ترميزه. ستعمل هذه الميزة فقط عندما يتم كبح الترميز. قم بأيقاف هذا الأعداد في حال واجهت مشاكل بتشغيل ألصوت أو ألفديو.", + "LabelThrottleDelaySeconds": "أكبح بعد", + "LabelThrottleDelaySecondsHelp": "ألزمن بالثواني الذي سيتم بعده كبح أعادة الترميز. يجب أن يكون الزمن طويلاً بكفاية ليحافظ العميل على مخزون صحي. يجب أن يكون كفح أعادة الترميز مفعلاً ليعمل هذا ألاعاداد.", + "LabelSegmentKeepSeconds": "الممدة للأحتفاظ على الشرائح", + "LabelEnableAudioVbrHelp": "معدل البِت المتغير ينتج على جودة أفضل مقارنة بمعدل البت المتوسط، ولكن في بعض الحالات النادرة قد يسبب مشاكل في التخزين المؤقت والتوافق.", + "LabelSegmentKeepSecondsHelp": "الزمن بالثواني الذي يجب الاحتفاظ به للشرائح قبل أن يتم الكتابة فوقها. يجب أن يكون أكبر من \"بعد الخنق\". يعمل هذا ألأعداد فقط إذا كان حذف الشرائح مفعلًا.", + "AiTranslated": "مترجمة من قبل ذكاء اسطناعي" } diff --git a/src/strings/as.json b/src/strings/as.json index 8a1f2b0690..020baefba5 100644 --- a/src/strings/as.json +++ b/src/strings/as.json @@ -1,4 +1,16 @@ { "Actor": "অভিনেতা", - "Absolute": "নিৰংকুশ" + "Absolute": "নিৰংকুশ", + "HeaderAlbumArtists": "অ্যালবাম শিল্পী", + "Albums": "এলবাম", + "Books": "পুস্তক", + "Channels": "চেনেলস", + "Movies": "চলচ্চিত্ৰ", + "Artists": "শিল্পী", + "Collections": "সংগ্রহ", + "Default": "ডিফল্ট", + "Favorites": "পছন্দসই", + "Folders": "ফোল্ডাৰ", + "Genres": "শ্রেণী", + "HeaderContinueWatching": "দেখা চালিয়ে যান" } diff --git a/src/strings/bg-bg.json b/src/strings/bg-bg.json index f9f61bb285..21502d6402 100644 --- a/src/strings/bg-bg.json +++ b/src/strings/bg-bg.json @@ -528,7 +528,7 @@ "Screenshots": "Снимки на екрана", "Search": "Търсене", "SearchForCollectionInternetMetadata": "Търсене в интернет за картини и метаданни", - "SearchForMissingMetadata": "Търсене за лисващи метаданни", + "SearchForMissingMetadata": "Търсене за липсващи метаданни", "SearchForSubtitles": "Търсене на субтитри", "SendMessage": "Изпращане на съобщение", "SeriesYearToPresent": "{0} - Настояще", @@ -1166,11 +1166,11 @@ "MessageUnsetContentHelp": "Съдържанието ще се показва като обикновени папки. За най-добри резултати използвайте мениджъра на метаданни, за да зададете типовете съдържание на подпапките.", "MessageUnableToConnectToServer": "В момента не можем да се свържем с избрания сървър. Моля, уверете се, че работи и опитайте отново.", "MessageReenableUser": "Вижте по-долу, за да активирате отново", - "MessagePluginInstallDisclaimer": "Приставките, създадени от членове на общността, са чудесен начин да подобрите изживяването с Джелифин чрез допълнителните функции и предимства.Преди да инсталирате, имайте предвид ефектите, които те могат да имат върху вашия Джелифин сървър, като по-дълго време за сканиране на библиотеки, допълнителна обработка на заден фон и намалена стабилност на системата.", + "MessagePluginInstallDisclaimer": "ПРЕДУПРЕЖДЕНИЕ: Инсталирането на плъгин на трета страна носи рискове. Може да съдържа нестабилен или злонамерен код и може да се промени по всяко време. Инсталирайте само плъгини от автори на които имате доверие! Имайте предвид потенциалните ефекти, които може да има, включително заявки към външни услуги, по-дълги сканирания на библиотеки или допълнителна фонова обработка.", "MessagePluginConfigurationRequiresLocalAccess": "За да конфигурирате тази приставка, моля, впишете се директно в локалния си сървър.", "MessagePleaseWait": "Моля,изчакайте. Това може да отнеме минута.", "MessagePlayAccessRestricted": "Възпроизвеждането на това съдържание в момента е ограничено.Моля, свържете се с администратора на вашия сървър за повече информация.", - "MessagePasswordResetForUsers": "Следните потребители са занулили паролите си.Те вече могат да влязат с пин кодовете, използвани за извършване на нулирането.", + "MessagePasswordResetForUsers": "Следните потребители са занулили паролите си.Те вече могат да влязат с ПИН кодовете, използвани за извършване на нулирането.", "MessageNoTrailersFound": "За да подобрите филмовото изживяване инсталирайте канал за трейлъри,може да подредите няколко канала в библиотека.", "MessageNoServersAvailable": "Не са намерени сървъри, използващи функцията за автоматично откриване на сървър.", "MessageNoMovieSuggestionsAvailable": "Понастоящем няма предложени филми. Започнете да гледате и оценявате филмите си, а след това се върнете, за да видите препоръките си.", @@ -1452,7 +1452,7 @@ "LabelAlbumArtMaxResHelp": "Максимална резолюция на изображенията предоставена чрез \"upnp:albumArtURI\" полето.", "KnownProxiesHelp": "Списък от IP ареси или хост имена на известни прокси сървъри, разделени със запетая, използвани при свързване с Jellyfin сървър. Това е задължително за да се използва правилнен \"X-Forwarded-For\" хедър. Изисква рестартиране след прилагане.", "HomeVideosPhotos": "Домашни видеа и снимки", - "DirectPlayHelp": "Основният файл е напълно съвместим с този клиент, което значи че го получавате без модификации.", + "DirectPlayHelp": "Премахване на изображение", "AllowTonemappingHelp": "Тоналното картографиране може да трансформира динамичния обхват на видеото от HDR към SDR, като същевременно запазва детайлите и цветовете на изображението, които са много важна информация за представяне на оригиналната сцена. В момента работи само с 10-битови HDR10,HLG и DoVi видеоклипове. Това изисква съответното време за изпълнение от OpenCL или CUDA.", "LabelMaxAudiobookResumeHelp": "Приема се ,че файловете се възпроизведени до края , ако се спре след като оставащото време е по-малко от тази стойност.", "Experimental": "Експериментални", @@ -1478,5 +1478,10 @@ "EnableAudioNormalizationHelp": "Нормализацията на звука ще усили сигналът за да поддържа средните честоти на желано ниво (-18dB).", "EnableAudioNormalization": "Нормализация на звука", "Unknown": "Неизвестен", - "LabelThrottleDelaySeconds": "Ограничи след" + "LabelThrottleDelaySeconds": "Ограничи след", + "GetThePlugin": "Вземете приставката", + "LabelLocalCustomCss": "Персонализиран CSS код за стилизиране, който се отнася само за този клиент. Може да искате да деактивирате персонализирания CSS код на сървъра.", + "LabelOriginalName": "Оригинално име", + "LabelQuickConnectCode": "Код за бързо свързване", + "LabelMaxVideoResolution": "Максимално разрешена разделителна способност на транскодиране на видео" } diff --git a/src/strings/ca.json b/src/strings/ca.json index 995dc1a0a1..23482272ac 100644 --- a/src/strings/ca.json +++ b/src/strings/ca.json @@ -1731,5 +1731,10 @@ "Notifications": "Notificacions", "NotificationsMovedMessage": "Les funcions de notificacions s'han mogut al plugin Webhook.", "UserMenu": "Menú d'usuari", - "AllowCollectionManagement": "Permet a aquest usuari gestionar col·leccions" + "AllowCollectionManagement": "Permet a aquest usuari gestionar col·leccions", + "AllowSegmentDeletion": "Suprimeix segments", + "AllowSegmentDeletionHelp": "Suprimeix els segments antics després d'haver estat enviats al client. Això impedeix haver d'emmagatzemar tot el fitxer transcodificat al disc. Només funcionarà amb l'activació de transcodes de l'acceleració. Desactiveu-ho si teniu problemes de reproducció.", + "LabelThrottleDelaySeconds": "Acceleració després de", + "LabelThrottleDelaySecondsHelp": "Temps en segons després del qual el transcodificador s'accelerarà. Ha de ser prou gran perquè el client mantingui una memòria intermèdia saludable. Només funciona si s'habilita l'acceleració.", + "LabelSegmentKeepSeconds": "Temps per mantenir els segments" } diff --git a/src/strings/cs.json b/src/strings/cs.json index 1237690855..6d04706b15 100644 --- a/src/strings/cs.json +++ b/src/strings/cs.json @@ -336,7 +336,7 @@ "Help": "Nápověda", "Hide": "Skrýt", "HideWatchedContentFromLatestMedia": "Skrýt zhlédnuté položky v seznamu Nedávno přidaná média", - "Home": "Domů", + "Home": "Domovská obrazovka", "Identify": "Identifikovat", "Images": "Obrázky", "ImportFavoriteChannelsHelp": "Jen kanály označené jako oblíbené na zařízení tuneru budou importovány.", @@ -1112,7 +1112,7 @@ "LabelSortOrder": "Pořadí řazení", "LabelSpecialSeasonsDisplayName": "Zobrazovaný název pro zvláštní sezónu", "LabelSubtitleDownloaders": "Stahovače titulků", - "LabelTVHomeScreen": "Hlavní obrazovka TV režimu", + "LabelTVHomeScreen": "Domovská obrazovka TV režimu", "LabelTag": "Tag", "LabelTypeMetadataDownloaders": "Stahovače metadat ({0})", "LabelTypeText": "Text", @@ -1408,7 +1408,7 @@ "QuickConnectDescription": "Chcete-li se připojit snadněji, na přihlašovaném zařízení stiskněte tlačítko Rychlé připojení a zadejte níže uvedený kód.", "QuickConnectDeactivated": "Rychlé připojení bylo vypnuto dříve, než mohl být požadavek schválen", "QuickConnectAuthorizeFail": "Neznámý kód pro rychlé připojení", - "QuickConnectAuthorizeSuccess": "Požadavek autorizován", + "QuickConnectAuthorizeSuccess": "Vaše zařízení bylo úspěšně ověřeno!", "QuickConnectAuthorizeCode": "Pro přihlášení zadejte kód {0}", "QuickConnectActivationSuccessful": "Úspěšně aktivováno", "QuickConnect": "Rychlé připojení", @@ -1733,7 +1733,7 @@ "EnableAudioNormalizationHelp": "Normalizace hlasitosti udržuje průměrnou hlasitost na požadované úrovni (-18 dB) přidáním konstantního zisku.", "EnableAudioNormalization": "Normalizace hlasitosti", "LabelEnableLUFSScan": "Povolit skenování LUFS", - "LabelEnableLUFSScanHelp": "Povolit tvorbu informací LUFS při skenování hudby. Prodlužuje skenování a je náročnější na výkon.", + "LabelEnableLUFSScanHelp": "Umožňuje klientům normalizovat hlasitost u přehrávaných skladeb. Prodlužuje skenování knihovny a je náročnější na výkon.", "GetThePlugin": "Získat zásuvný modul", "Notifications": "Oznámení", "NotificationsMovedMessage": "Funkce oznámení se přesunula do zásuvného modulu Webhook.", @@ -1771,5 +1771,10 @@ "ForeignPartsOnly": "Pouze vynucené", "HearingImpairedShort": "Titulky pro neslyšící", "AiTranslated": "Přeložené pomocí UI", - "HeaderGuestCast": "Zvláštní hosté" + "HeaderGuestCast": "Zvláštní hosté", + "GoHome": "Přejít na domovskou obrazovku", + "UnknownError": "Došlo k neznámé chybě.", + "BackdropScreensaver": "Pozadí", + "LogoScreensaver": "Logo", + "LabelIsHearingImpaired": "Titulky pro neslyšící" } diff --git a/src/strings/da.json b/src/strings/da.json index 5bed42a5c8..0b4176d23e 100644 --- a/src/strings/da.json +++ b/src/strings/da.json @@ -926,7 +926,7 @@ "Collections": "Samlinger", "Favorites": "Favoritter", "Folders": "Mapper", - "HeaderAlbumArtists": "Albums kunstnere", + "HeaderAlbumArtists": "Albumkunstnere", "Absolute": "Absolut", "AccessRestrictedTryAgainLater": "Adgang er begrænset lige nu. Prøv venligst igen senere.", "Aired": "Udgivet", @@ -1336,7 +1336,7 @@ "EnableFasterAnimations": "Hurtigere animationer", "DisablePlugin": "Deaktiver", "EnablePlugin": "Aktiver", - "DirectPlayHelp": "Kilde filen er kompatibel med denne klient, og modtager filen uden brug af omkodning.", + "DirectPlayHelp": "Kilde filen er kompatibel med denne klient og modtager filen uden brug af omkodning.", "EnableEnhancedNvdecDecoder": "Aktiver forbedret NVDEC-dekoder", "MessagePlaybackError": "Der opstod en fejl under afspilning af denne fil på din Google Cast modtager.", "MessageChromecastConnectionError": "Din Google Cast modtager kan ikke komme i kontakt med Jellyfin serveren. Undersøg venligst forbindelsen og prøv igen.", @@ -1702,5 +1702,19 @@ "SubtitleCyan": "Cyan", "SubtitleMagenta": "Magenta", "AllowCollectionManagement": "Tillad denne bruger at administrere samlinger", - "AllowSegmentDeletion": "Slet segmenter" + "AllowSegmentDeletion": "Slet segmenter", + "HeaderEpisodesStatus": "Episodestatus", + "GoHome": "Gå Hjem", + "EnableAudioNormalizationHelp": "Audionormalisering tilføjer en konstant forstærkning for at holde gennemsnittet på et ønsket niveau (-18 dB).", + "EnableAudioNormalization": "Audio Normalisering", + "GridView": "Gittervisning", + "HeaderConfirmRepositoryInstallation": "Bekræft installation af plugin-repositorium", + "BackdropScreensaver": "Screensaver baggrund", + "GetThePlugin": "Få pluginnet", + "AllowSegmentDeletionHelp": "Slet gamle segmenter, når de er blevet sendt til klienten. Dette forhindrer, at man skal gemme hele den transkodede fil på disken. Fungerer kun med throttling aktiveret. Slå dette fra, hvis du oplever afspilningsproblemer.", + "LabelThrottleDelaySeconds": "Begræns efter", + "LabelThrottleDelaySecondsHelp": "Tid i sekunder, hvorefter transcoderen vil blive begrænset. Skal være stor nok til, at klienten kan opretholde en sund buffer. Virker kun, hvis throttling er aktiveret.", + "LabelSegmentKeepSeconds": "Tid at gemme segmenter i", + "LabelSegmentKeepSecondsHelp": "Tid i sekunder, som segmenter skal gemmes i, før de overskrives. Skal være større end \"Begræns efter\". Virker kun, hvis sletning af segmenter er aktiveret.", + "HeaderGuestCast": "Gæstestjerner" } diff --git a/src/strings/de.json b/src/strings/de.json index 6f4d185349..0e2dd77491 100644 --- a/src/strings/de.json +++ b/src/strings/de.json @@ -1377,7 +1377,7 @@ "LabelColorSpace": "Farbraum", "MediaInfoColorSpace": "Farbraum", "VideoAudio": "Videoton", - "AllowTonemappingHelp": "Tone-Mapping kann den Dynamikumfang eines Videos von HDR nach SDR wandeln und dabei die für die Darstellung der Originalszene sehr wichtigen Bilddetails und Farben beibehalten. Dies funktioniert zurzeit nur bei HDR10-, HLG- und Dolby-Vision-Videos und benötigt die entsprechende OpenCL- oder CUDA-Laufzeitumgebung.", + "AllowTonemappingHelp": "Tone-Mapping kann den Dynamikumfang eines Videos von HDR nach SDR wandeln und dabei die für die Darstellung der Originalszene sehr wichtigen Bilddetails und Farben beibehalten. Dies funktioniert zurzeit nur bei HDR10, HLG und Dolby-Vision Videos und benötigt die entsprechende OpenCL- oder CUDA-Laufzeitumgebung.", "TonemappingRangeHelp": "Wähle den Ausgabefarbraum aus. Auto ist derselbe wie der Eingabefarbraum.", "TonemappingAlgorithmHelp": "Das Tone-Mapping kann fein abgestimmt werden. Wenn du mit diesen Optionen nicht vertraut bist, behalte einfach den Standardwert bei. Der empfohlene Wert ist \"BT.2390\".", "LabelTonemappingAlgorithm": "Wähle den zu verwendenden Tone-Mapping-Algorithmus aus", @@ -1407,7 +1407,7 @@ "QuickConnectDescription": "Für das Einloggen mit Quick Connect wähle den 'Quick Connect'-Knopf auf deinem Gerät, mit dem du dich anmelden möchtest, und gib den unten angezeigten Code ein.", "QuickConnectDeactivated": "Quick Connect wurde deaktiviert, bevor der Login verifiziert werden konnte", "QuickConnectAuthorizeFail": "Unbekannter Quick Connect-Code", - "QuickConnectAuthorizeSuccess": "Anfrage autorisiert", + "QuickConnectAuthorizeSuccess": "Das Gerät wurde erfolgreich authentifiziert!", "QuickConnectAuthorizeCode": "Login Code {0} eingeben", "QuickConnectActivationSuccessful": "Erfolgreich aktiviert", "EnableQuickConnect": "Quick Connect auf diesem Server aktivieren", @@ -1733,7 +1733,7 @@ "PasswordRequiredForAdmin": "Für Admin Konten wird ein Passwort benötigt.", "LabelEnableLUFSScan": "LUFS-Scan aktivieren", "LabelSyncPlayNoGroups": "Keine Gruppen verfügbar", - "LabelEnableLUFSScanHelp": "Aktiviert den LUFS-Scan für Musik (Dies erfordert mehr Zeit und Ressourcen).", + "LabelEnableLUFSScanHelp": "Clients können die Audio Wiedergabe normalisieren, um die selbe Lautstärke für mehrere Stücke zu bekommen.\nDies verlängert den Bibliotheksscan und benötigt mehr Ressourcen.", "Notifications": "Benachrichtigungen", "NotificationsMovedMessage": "Die Benachrichtigungsfunktion wurde zum Webhook Plugin verschoben.", "EnableAudioNormalizationHelp": "Die Audionormalisierung fügt eine konstante Verstärkung hinzu, um den Durchschnitt auf einem gewünschten Pegel zu halten (-18 dB).", @@ -1758,8 +1758,23 @@ "HeaderEpisodesStatus": "Episodenstatus", "AllowSegmentDeletion": "Segmente löschen", "AllowSegmentDeletionHelp": "Alte Segmente löschen, nachdem sie zum Client gesendet wurden. Damit muss nicht die gesamte transkodierte Datei zwischengespeichert werden. Sollten Wiedergabeprobleme auftreten, kann diese Einstellung deaktiviert werden.", - "LabelThrottleDelaySeconds": "Limitieren nach", - "LabelThrottleDelaySecondsHelp": "Zeit, in Sekunden, nach der die Transkodierung limitiert wird. Muss groß genug sein um dem Client eine problemlose Wiedergabe zu ermöglichen. Funktioniert nur wenn \"Transkodierung drosseln\" aktiviert ist.", + "LabelThrottleDelaySeconds": "Drosseln nach", + "LabelThrottleDelaySecondsHelp": "Zeit, in Sekunden, nach der die Transkodierung gedrosselt wird. Muss groß genug sein um dem Client eine problemlose Wiedergabe zu ermöglichen. Funktioniert nur wenn \"Transkodierung drosseln\" aktiviert ist.", "LabelSegmentKeepSeconds": "Zeit um Segmente zu behalten", - "LabelSegmentKeepSecondsHelp": "Zeit, in Sekunden, in der Segmente nicht überschrieben werden dürfen. Muss größer sein als \"Limitieren nach\". Funktioniert nur wenn \"Segmente löschen\" aktiviert ist." + "LabelSegmentKeepSecondsHelp": "Zeit, in Sekunden, in der Segmente nicht überschrieben werden dürfen. Muss größer sein als \"Limitieren nach\". Funktioniert nur wenn \"Segmente löschen\" aktiviert ist.", + "LogoScreensaver": "Logo Bildschirmschoner", + "UnknownError": "Ein unbekannter Fehler trat auf.", + "GridView": "Kachelansicht", + "ListView": "Listenansicht", + "GoHome": "Startseite", + "AiTranslated": "AI übersetzt", + "MachineTranslated": "maschinenübersetzt", + "AllowAv1Encoding": "Encodierung ins AV1 Format erlauben", + "LabelIsHearingImpaired": "Für Hörgeschädigte (SDH)", + "LabelBackdropScreensaverInterval": "Hintergrund-Bildschirmschoner-Intervall", + "BackdropScreensaver": "Hintergrund Bildschirmschoner", + "ForeignPartsOnly": "Erzwungen/Nur ausländische Teile", + "HearingImpairedShort": "BaFa/SDH", + "HeaderGuestCast": "Gast Stars", + "LabelBackdropScreensaverIntervalHelp": "Die Zeit in Sekunden zwischen dem Wechsel verschiedener Hintergrundbilder im Bildschirmschoner." } diff --git a/src/strings/el.json b/src/strings/el.json index 93c12cebf7..152e3147b7 100644 --- a/src/strings/el.json +++ b/src/strings/el.json @@ -1696,5 +1696,7 @@ "LabelDummyChapterCount": "Όριο", "LabelDummyChapterCountHelp": "Ο μέγιστος αριθμός εικόνων κεφαλαίου που θα εξαχθεί για κάθε αρχείο πολυμέσων.", "LabelChapterImageResolution": "Ανάλυση", - "LabelChapterImageResolutionHelp": "Η ανάλυση των εξαγόμενων εικόνων κεφαλαίου." + "LabelChapterImageResolutionHelp": "Η ανάλυση των εξαγόμενων εικόνων κεφαλαίου.", + "AllowCollectionManagement": "Επίτρεψε στον χρήστη να διαχειρίζεται συλλογές", + "AllowSegmentDeletion": "Διαγραφή τμημάτων" } diff --git a/src/strings/en-gb.json b/src/strings/en-gb.json index 6aeccaaaa0..3725023495 100644 --- a/src/strings/en-gb.json +++ b/src/strings/en-gb.json @@ -1392,7 +1392,7 @@ "LabelTonemappingRange": "Tone mapping range", "TonemappingAlgorithmHelp": "Tone mapping can be fine-tuned. If you are not familiar with these options, just keep the default. The recommended value is 'BT.2390'.", "LabelTonemappingAlgorithm": "Select the Tone mapping algorithm to use", - "AllowTonemappingHelp": "Tone mapping can transform the dynamic range of a video from HDR to SDR while maintaining image details and colours, which are very important information for representing the original scene. Currently works only with 10bit HDR10,HLG and DoVi videos. This requires the corresponding OpenCL or CUDA runtime.", + "AllowTonemappingHelp": "Tone mapping can transform the dynamic range of a video from HDR to SDR while maintaining image details and colours, which are very important information for representing the original scene. Currently works only with 10bit HDR10, HLG and DoVi videos. This requires the corresponding OpenCL or CUDA runtime.", "EnableTonemapping": "Enable Tone mapping", "LabelOpenclDeviceHelp": "This is the OpenCL device that is used for tone mapping. The left side of the dot is the platform number, and the right side is the device number on the platform. The default value is 0.0. The FFmpeg application file containing the OpenCL hardware acceleration method is required.", "LabelOpenclDevice": "OpenCL Device", @@ -1411,7 +1411,7 @@ "QuickConnectDescription": "To sign in with Quick Connect, select the 'Quick Connect' button on the device you are logging in from and enter the displayed code below.", "QuickConnectDeactivated": "Quick Connect was deactivated before the login request could be approved", "QuickConnectAuthorizeFail": "Unknown Quick Connect code", - "QuickConnectAuthorizeSuccess": "Request authorised", + "QuickConnectAuthorizeSuccess": "You have successfully authenticated your device!", "QuickConnectAuthorizeCode": "Enter code {0} to login", "QuickConnectActivationSuccessful": "Successfully activated", "QuickConnect": "Quick Connect", @@ -1761,5 +1761,17 @@ "LabelThrottleDelaySeconds": "Throttle after", "LabelThrottleDelaySecondsHelp": "Time in seconds after which the transcoder will be throttled. Must be large enough for the client to maintain a healthy buffer. Only works if throttling is enabled.", "LabelSegmentKeepSeconds": "Time to keep segments", - "LabelSegmentKeepSecondsHelp": "Time in seconds for which segments should be kept before they are overwritten. Must be greater than \"Throttle after\". Only works if segment deletion is enabled." + "LabelSegmentKeepSecondsHelp": "Time in seconds for which segments should be kept before they are overwritten. Must be greater than \"Throttle after\". Only works if segment deletion is enabled.", + "AllowAv1Encoding": "Allow encoding in AV1 format", + "GoHome": "Go Home", + "UnknownError": "An unknown error occurred.", + "GridView": "Grid View", + "ListView": "List View", + "LabelBackdropScreensaverInterval": "Backdrop Screensaver Interval", + "LabelBackdropScreensaverIntervalHelp": "The time in seconds between different backdrops when using the backdrop screensaver.", + "AiTranslated": "AI Translated", + "MachineTranslated": "Machine Translated", + "ForeignPartsOnly": "Forced/Foreign parts only", + "HearingImpairedShort": "HI/SDH", + "HeaderGuestCast": "Guest Stars" } diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 7f4da8765a..46ac586951 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -59,6 +59,7 @@ "Auto": "Auto", "Backdrop": "Backdrop", "Backdrops": "Backdrops", + "BackdropScreensaver": "Backdrop Screensaver", "Banner": "Banner", "BirthDateValue": "Born: {0}", "BirthLocation": "Birth location", @@ -297,6 +298,7 @@ "Genre": "Genre", "Genres": "Genres", "GetThePlugin": "Get the Plugin", + "GoHome": "Go Home", "GoogleCastUnsupported": "Google Cast Unsupported", "GridView": "Grid View", "GroupBySeries": "Group by series", @@ -666,7 +668,7 @@ "LabelEnableIP6": "Enable IPv6", "LabelEnableIP6Help": "Enable IPv6 functionality.", "LabelEnableLUFSScan": "Enable LUFS scan", - "LabelEnableLUFSScanHelp": "Enable LUFS scan for music (This will take longer and more resources).", + "LabelEnableLUFSScanHelp": "Clients can normalize audio playback to get equal loudness across tracks. This will make library scans longer and take more resources.", "LabelEnableRealtimeMonitor": "Enable real time monitoring", "LabelEnableRealtimeMonitorHelp": "Changes to files will be processed immediately on supported file systems.", "LabelEnableSingleImageInDidlLimit": "Limit to single embedded image", @@ -1015,6 +1017,7 @@ "LogLevel.Critical": "Critical", "LogLevel.None": "None", "Logo": "Logo", + "LogoScreensaver": "Logo Screensaver", "Lyricist": "Lyricist", "ManageLibrary": "Manage library", "ManageRecording": "Manage recording", @@ -1361,7 +1364,7 @@ "QuickConnectActivationSuccessful": "Successfully activated", "QuickConnectAuthorizeCode": "Enter code {0} to login", "QuickConnectAuthorizeFail": "Unknown Quick Connect code", - "QuickConnectAuthorizeSuccess": "Request authorized", + "QuickConnectAuthorizeSuccess": "You have successfully authenticated your device!", "QuickConnectDeactivated": "Quick Connect was deactivated before the login request could be approved", "QuickConnectDescription": "To sign in with Quick Connect, select the 'Quick Connect' button on the device you are logging in from and enter the displayed code below.", "QuickConnectInvalidCode": "Invalid Quick Connect code", @@ -1557,6 +1560,7 @@ "Typewriter": "Typewriter", "Uniform": "Uniform", "UninstallPluginConfirmation": "Are you sure you wish to uninstall {0}?", + "UnknownError": "An unknown error occurred.", "Unmute": "Unmute", "Unplayed": "Unplayed", "Unrated": "Unrated", @@ -1723,5 +1727,6 @@ "AiTranslated": "AI Translated", "MachineTranslated": "Machine Translated", "ForeignPartsOnly": "Forced/Foreign parts only", - "HearingImpairedShort": "HI/SDH" + "HearingImpairedShort": "HI/SDH", + "LabelIsHearingImpaired": "For hearing impaired (SDH)" } diff --git a/src/strings/es-mx.json b/src/strings/es-mx.json index 93e8c31426..3fb0abb6a3 100644 --- a/src/strings/es-mx.json +++ b/src/strings/es-mx.json @@ -1400,7 +1400,7 @@ "QuickConnectDescription": "Para entrar con Conexión Rápida, selecciona el botón de 'Conexión rápida' en el dispositivo desde donde intentas entrar e introduce el código que se muestra debajo.", "QuickConnectDeactivated": "La Conexión Rápida fue desactivada antes de que la petición pudiera ser aprobada", "QuickConnectAuthorizeFail": "Código de Conexión Rápida desconocido", - "QuickConnectAuthorizeSuccess": "Petición autorizada", + "QuickConnectAuthorizeSuccess": "¡Has autenticado tu dispositivo exitosamente!", "QuickConnectAuthorizeCode": "Introduce el codigo {0} para entrar", "QuickConnectActivationSuccessful": "Activada exitosamente", "QuickConnect": "Conexión rápida", @@ -1697,10 +1697,10 @@ "SaveRecordingNFOHelp": "Guardar metadatos del proveedor de listados EPG junto con los archivos multimedia.", "VideoBitrateNotSupported": "La taza de bits del vídeo no es compatible", "UnknownVideoStreamInfo": "La información de transmisión de video es desconocida", - "EnableAudioNormalizationHelp": "La normalización de audio añadirá una ganancia constante para mantener la media en el nivel deseado (-18dB)", + "EnableAudioNormalizationHelp": "La normalización de audio añadirá una ganancia constante para mantener la media en el nivel deseado (-18dB).", "HeaderPerformance": "Rendimiento", "LabelEnableLUFSScan": "Habilitar escaneó LUFS", - "LabelEnableLUFSScanHelp": "Habilitar escaneó LUFS para musica (Esto tomara más tiempo y usará más recursos).", + "LabelEnableLUFSScanHelp": "Los clientes pueden normalizar la reproducción de audio para tener el mismo nivel de ruido entre las pistas de audio. Esto hará que los escaneos de biblioteca tomen más tiempo y recursos.", "MenuClose": "Cerrar Menu", "MenuOpen": "Abrir Menu", "SubtitleBlack": "Negro", @@ -1755,9 +1755,24 @@ "TonemappingModeHelp": "Seleccione el modo de mapeado de tono. Si experimenta sobreiluminación intente cambiar al modo RGB.", "AllowSegmentDeletion": "Borrar segmentos", "HeaderEpisodesStatus": "Estatus de los Episodios", - "AllowSegmentDeletionHelp": "Borrar los viejos segmentos después de que hayan sido enviados al cliente. Esto previene que se tenga almacenado la totalidad de la transcodificación en el disco. Esto funciona unicamente cuando se tenga habilitado el throttling. Apagar esta opción cuando se tengan problemas de reproducción.", + "AllowSegmentDeletionHelp": "Borrar los viejos segmentos después de que hayan sido enviados al cliente. Esto previene que se tenga almacenado la totalidad de la transcodificación en el disco. Esto funciona unicamente cuando se tenga habilitado el throttling. Apagar esta opción cuando se tengan problemas de reproducción.", "LabelThrottleDelaySeconds": "Acelerar después", - "LabelThrottleDelaySecondsHelp": "Tiempo en segundos después de que la transcodificación entre en throttled. Deben ser los suficientes para que el buffer del cliente siga operando. Unicamente funciona si el throtting está habilitdo.", - "LabelSegmentKeepSeconds": "Tiempo para que permanescan los segmentos", - "LabelSegmentKeepSecondsHelp": "Tiempo en segundos en los que los segmentos deben permanecer antes de que sean sobrescritos. Estos deben de ser mayores a los indicados en \"Acelerar despues de\". Esto funciona unicamente si esta habilitada la opción de eliminar el segmento." + "LabelThrottleDelaySecondsHelp": "Tiempo en segundos después de que la transcodificación entre en aceleración. Deben ser los suficientes para que el buffer del cliente siga operando. Unicamente funciona si la aceleración está habilitada.", + "LabelSegmentKeepSeconds": "Tiempo para guardar segmentos", + "LabelSegmentKeepSecondsHelp": "Tiempo en segundos en los que los segmentos deben permanecer antes de que sean sobrescritos. Estos deben de ser mayores a los indicados en \"Acelerar despues de\". Esto funciona unicamente si esta habilitada la opción de eliminar el segmento.", + "AllowAv1Encoding": "Permitir encodificación en formato AV1", + "GoHome": "Ir a Inicio", + "UnknownError": "Un error desconocido ocurrió.", + "GridView": "Vista en Cuadrícula", + "LabelBackdropScreensaverInterval": "Intervalo del Protector de Pantalla de Fondo", + "BackdropScreensaver": "Protector de pantalla de Fondo", + "LabelBackdropScreensaverIntervalHelp": "El tiempo en segundos entre diferentes fondos cuando se usa el protector de pantalla de fondo.", + "ListView": "Vista en Lista", + "LogoScreensaver": "Protector de Pantalla de Logo", + "AiTranslated": "Traducido por IA", + "MachineTranslated": "Traducido por Máquina", + "ForeignPartsOnly": "Solamente partes Forzadas/Foráneas", + "HearingImpairedShort": "HI/SDH", + "HeaderGuestCast": "Estrellas Invitadas", + "LabelIsHearingImpaired": "Para personas con discapacidad auditiva (SDH)" } diff --git a/src/strings/es.json b/src/strings/es.json index ecd3e6974d..ab3ac87d95 100644 --- a/src/strings/es.json +++ b/src/strings/es.json @@ -1378,7 +1378,7 @@ "QuickConnectDescription": "Para iniciar sesión con conexión rápida, pulsa el botón \"Conexión Rápida\" en el dispositivo desde el que está iniciando sesión e introduce el código que se muestra a continuación.", "QuickConnectDeactivated": "La conexión rápida se desactivó antes que se pudiera aprobar la solicitud de inicio de sesión", "QuickConnectAuthorizeFail": "Código de conexión rápida desconocido", - "QuickConnectAuthorizeSuccess": "Solicitar autorización", + "QuickConnectAuthorizeSuccess": "¡Tu dispositivo ha sido exitosamente autenticado!", "QuickConnectAuthorizeCode": "Introducir código de identificación {0}", "QuickConnectActivationSuccessful": "Activado satisfactoriamente", "QuickConnect": "Conexión rápida", @@ -1738,7 +1738,7 @@ "LabelSyncPlayNoGroups": "No hay grupos disponibles", "Notifications": "Notificaciones", "EnableAudioNormalization": "Normalización de audio", - "LabelEnableLUFSScanHelp": "Habilitar escaneo LUFS para música (Esto tardará más y consumirá más recursos).", + "LabelEnableLUFSScanHelp": "Los clientes pueden normalizar la reproducción de audio para obtener un nivel de ruido igual en todas las pistas de audio. Esto hará que la biblioteca escaneé por más tiempo y utilice más recursos.", "LabelDate": "Fecha", "LabelLevel": "Nivel", "LabelMediaDetails": "Detalles de medios", @@ -1763,5 +1763,18 @@ "LabelThrottleDelaySeconds": "Limitar trás", "LabelSegmentKeepSecondsHelp": "Tiempo en segundos durante el cual se deben conservar los segmentos antes de que se sobrescriban. Debe ser mayor que \"Acelerar después\". Solo funciona si la eliminación de segmentos está habilitada.", "LabelBackdropScreensaverInterval": "Intervalo del fondo protector de pantalla", - "LabelBackdropScreensaverIntervalHelp": "El tiempo en segundos entre diferentes fondos cuando se utiliza el fondo protector de pantalla." + "LabelBackdropScreensaverIntervalHelp": "El tiempo en segundos entre diferentes fondos cuando se utiliza el fondo protector de pantalla.", + "AllowAv1Encoding": "Permitir encodificación en formato AV1", + "GoHome": "Ir a Inicio", + "UnknownError": "Un error desconocido ocurrió.", + "GridView": "Vista en Cuadrícula", + "ListView": "Vista en Lista", + "BackdropScreensaver": "Protector de Pantalla de Fondo", + "LogoScreensaver": "Protector de Pantallas de Logo", + "HearingImpairedShort": "HI/SDH", + "AiTranslated": "Traducido por IA", + "MachineTranslated": "Traducido por Máquina", + "HeaderGuestCast": "Estrellas Invitadas", + "ForeignPartsOnly": "Partes Forzadas/Foráneas solamente", + "LabelIsHearingImpaired": "Para personas con discapacidad auditiva (SDH)" } diff --git a/src/strings/fi.json b/src/strings/fi.json index 501942ee4a..4ecc915066 100644 --- a/src/strings/fi.json +++ b/src/strings/fi.json @@ -911,7 +911,7 @@ "LabelAlbumArtists": "Albumin esittäjät", "Items": "Kohteet", "ItemCount": "{0} kohdetta", - "Home": "Koti", + "Home": "Aloitusnäyttö", "Help": "Apua", "HeaderXmlSettings": "XML-asetukset", "HeaderXmlDocumentAttributes": "XML-dokumentin attribuutit", @@ -1242,7 +1242,7 @@ "LabelIdentificationFieldHelp": "Regex-lauseke tai alaotsikko (kirjainten koolla ei välillä).", "LabelIconMaxResHelp": "'upnp:icon'-tietueen välityksellä näytettävien kuvakkeiden enimmäistarkkuus.", "LabelTVHomeScreen": "Televisiotilan aloitusnäyttö", - "LabelHomeScreenSectionValue": "Aloitusnäyttö-osio {0}", + "LabelHomeScreenSectionValue": "Aloitusnäytön osio {0}", "LabelHDHomerunPortRangeHelp": "Rajoittaa HD Homerun -laitteiden UDP-porttialueen tähän arvoon (oletus on 1024 - 65535).", "LabelExtractChaptersDuringLibraryScanHelp": "Pura kirjastopäivityksen yhteydessä tuotavien videoiden kappalekuvat. Muutoin tämä tapahtuu kappalekuvien purun ajoitetun tehtävän aikana, jolloin kirjaston perustarkastus nopeutuu.", "LabelHDHomerunPortRange": "HDHomeRun -portin alue", @@ -1386,7 +1386,7 @@ "MessageNoCollectionsAvailable": "Kokoelmien avulla voit nauttia elokuvien, sarjojen ja albumien mukautetuista ryhmityksistä. Luo kokoelmia painamalla '+'-painiketta.", "MessageLeaveEmptyToInherit": "Jätä tyhjäksi periäksesi asetukset ylemmän tason kohteesta tai globaalista oletusarvosta.", "MessageGetInstalledPluginsError": "Noudettaessa listaa asennetuista lisäosista tapahtui virhe.", - "MessageForgotPasswordInNetworkRequired": "Ole hyvä ja yritä uudestaan kotiverkossasi aloittaaksesi salasanan palautuksen.", + "MessageForgotPasswordInNetworkRequired": "Yritä aloittaa salasanan palautus uudelleen kotiverkostasi.", "MessageForgotPasswordFileCreated": "Seuraava tiedosto on luotu palvelimellesi, joka sisältää ohjeet jatkamiseen", "MessageFileReadError": "Luettaessa tiedostoa tapahtui virhe. Yritä uudelleen.", "MessageDirectoryPickerLinuxInstruction": "Linux-järjestelmille Arch linux, CentOS, Debian, Fedora, openSUSE tai Ubuntu, sinun on annettava palvelukäyttäjälle vähintään lukuoikeudet tallennustiloihisi.", @@ -1439,7 +1439,7 @@ "QuickConnectInvalidCode": "Virheellinen Pikayhdistyskoodi", "QuickConnectDescription": "Kirjautuaksesi Pikayhdistyksellä, valitse 'Pikayhdistys'-painike laitteelta, josta yrität kirjautua ja syötä alla oleva koodi.", "QuickConnectDeactivated": "Pikayhdistys katkaistiin ennen kirjautumispyynnön hyväksyntää", - "QuickConnectAuthorizeSuccess": "Pyyntö hyväksytty", + "QuickConnectAuthorizeSuccess": "Laitteesi on todennettu!", "QuickConnectAuthorizeFail": "Tuntematon Pikayhdistyskoodi", "QuickConnectAuthorizeCode": "Kirjaudu syöttämällä koodi {0}", "QuickConnectActivationSuccessful": "Käyttöönotto onnistui", @@ -1731,7 +1731,7 @@ "EnableAudioNormalizationHelp": "Äänen normalisointi asettaa äänelle kiinteän vahvistuksen keskivoimakkuuden vakiotasolla (-18 dB).", "EnableAudioNormalization": "Äänen normalisointi", "LabelEnableLUFSScan": "Suorita LUFS-tarkistus", - "LabelEnableLUFSScanHelp": "Käytä musiikin LUFS-tarkistusta (tämä vaatii enemmän aikaa ja resurseja).", + "LabelEnableLUFSScanHelp": "Päätteet voivat nyt tasoittaa ääniraitojen välisiä äänenvoimakkuuseroja. Tämä pidentää kirjastotarkistuksia ja kuormittaa laitteistoa enemmän.", "GetThePlugin": "Hanki lisäosa", "Notifications": "Ilmoitukset", "NotificationsMovedMessage": "Ilmoitustoiminnallisuus on siirtynyt Webhook-lisäosaan.", @@ -1769,5 +1769,10 @@ "MachineTranslated": "Konekäännetty", "ForeignPartsOnly": "Pakotettu/vain vieraskieliset osat", "HearingImpairedShort": "HI/SDH", - "HeaderGuestCast": "Vierailevat pääosat" + "HeaderGuestCast": "Vierailevat pääosat", + "GoHome": "Siirry aloitusnäyttöön", + "UnknownError": "Tapahtui tuntematon virhe.", + "BackdropScreensaver": "Backdrop-näytönsäästäjä", + "LogoScreensaver": "Logo-näytönsäästäjä", + "LabelIsHearingImpaired": "Kuulorajoitteisille (SDH)" } diff --git a/src/strings/fil.json b/src/strings/fil.json index 79f2daf59a..98ea9041e5 100644 --- a/src/strings/fil.json +++ b/src/strings/fil.json @@ -1666,5 +1666,9 @@ "HeaderPerformance": "Pagsasagawa", "LabelStereoDownmixAlgorithm": "Stereo Downmix Algorithm", "LabelDummyChapterCount": "Hangganan", - "LabelDummyChapterCountHelp": "Ang pinakamataas na bilang ng mga larawan ng kabanata na kukunin para sa bawat media file." + "LabelDummyChapterCountHelp": "Ang pinakamataas na bilang ng mga larawan ng kabanata na kukunin para sa bawat media file.", + "AllowCollectionManagement": "Payagan ang user na pangasiwaan ang koleksyon", + "AllowSegmentDeletion": "Burahin ang segment", + "AllowSegmentDeletionHelp": "Burahin ang lumang segment pagkatapos mapadala sa kliyente. Pinipigilan nito ang pag-imbak ng buong transcoded file sa disk. Gumagana lamang kapag nakabukas ang throttling. Patayin ito kung nakakaranas ng problema sa playback.", + "LabelThrottleDelaySeconds": "I-throttle pagkatapos" } diff --git a/src/strings/fo.json b/src/strings/fo.json new file mode 100644 index 0000000000..6532f3e13a --- /dev/null +++ b/src/strings/fo.json @@ -0,0 +1,216 @@ +{ + "AccessRestrictedTryAgainLater": "Atgongd er avmarkað. Vinaliga royn aftur seinni.", + "Actor": "Sjónleikari", + "Add": "Legg afturat", + "AddedOnValue": "{0} lagt afturat", + "AddToCollection": "Koyr í samling", + "AddToFavorites": "Legg til yndislistan", + "AddToPlaylist": "Legg til spælilistan", + "Alerts": "Ávaringar", + "All": "Øll", + "AllEpisodes": "Allir partar", + "AllLanguages": "Øll tungumál", + "LabelTonemappingMode": "Tónaavmyndingarháttur", + "HearingImpairedShort": "Hoyriveik/SDH", + "MachineTranslated": "Maskin týðing", + "AiTranslated": "Vitlíkistýðing", + "AllowAv1Encoding": "Loyva koding í AV1 bygnaði", + "LabelIsHearingImpaired": "Til hoyriveik (SDH)", + "Unknown": "Ókend", + "TonemappingModeHelp": "Vel tónaavmyndingarháttin. Um tú verður fyri útblástum hálýsingum, royn so heldur RGB-støðuna.", + "Unreleased": "Ikki latið út enn", + "AlbumArtist": "Album Listafólk", + "AllChannels": "Allar rásir", + "AllComplexFormats": "Allar Kompleksu Formatir (ASS, SSA, VobSub, PGS, SUB, IDX, ...)", + "Directors": "Leikstjórar", + "AgeValue": "({0} ára gamalt)", + "AllLibraries": "Øll søvn", + "Artist": "Listafólk", + "Artists": "Listafólk", + "Books": "Bøkur", + "Composer": "Tónaskald", + "DailyAt": "Dagligani kl. {0}", + "DashboardVersionNumber": "Útgáva: {0}", + "DeathDateValue": "Deyð(ur): {0}", + "Digital": "Talgilt", + "Director": "Leikstjóri", + "Friday": "Fríggjadag", + "HeaderAdmin": "Umsiting", + "HeaderDevices": "Eindir", + "HeaderError": "Feilur", + "HeaderForKids": "Fyri Børn", + "AllowSegmentDeletion": "Strika partar", + "LabelThrottleDelaySeconds": "Kyrkja", + "AllowMediaConversion": "miðla", + "AllowOnTheFlySubtitleExtraction": "undirteksta", + "AllowCollectionManagement": "brúkara", + "AllowFfmpegThrottling": "Kyrkja", + "AllowFfmpegThrottlingHelp": "umkoding avspæling avspælingar", + "AllowSegmentDeletionHelp": "avspælingar", + "LabelSegmentKeepSecondsHelp": "Kyrkja parta.", + "AllowHWTranscodingHelp": "avkoda ambætarinum", + "AllowMediaConversionHelp": "miðla", + "AllowOnTheFlySubtitleExtractionHelp": "Íkervnir undirtekstir avspæling íkervnar undirtekstir", + "AllowRemoteAccess": "ambætaran", + "HeaderPassword": "Loyniorð", + "HeaderLibraries": "Søvn", + "HeaderParentalRatings": "Aldursmark", + "HeaderSecondsValue": "{0} sekund", + "HeaderSendMessage": "Send boð", + "Kids": "Børn", + "LabelArtists": "Listafólk", + "LabelCountry": "Land", + "LabelDeveloper": "Mennari", + "Absolute": "Absolut", + "AddToPlayQueue": "Legg til spæl bíðirøð", + "AirDate": "Útgávu ár", + "Aired": "Útgivið", + "Album": "Album", + "Albums": "Album", + "LabelSegmentKeepSeconds": "Tíð at halda petti", + "AroundTime": "Umleið {0}", + "Ascending": "Hækkandi", + "SearchForMissingMetadata": "Leita eftir manglandi metadata", + "SearchForSubtitles": "Leita eftir undirtekstið", + "SearchResults": "Leitiúrslit", + "SelectServer": "Vel ambætara", + "SendMessage": "Send boð", + "Season": "Sesong", + "SeriesDisplayOrderHelp": "Raða partar eftir dato, DVD ordan ella absolut nummerering.", + "ServerNameIsRestarting": "Ambætarin á {0} endurbyrjar.", + "ServerNameIsShuttingDown": "Ambætarin á {0} sløknar.", + "ServerUpdateNeeded": "Hesin ambætarin má dagførast. Fyri at heinta nýggjastu útgávuna, vinarliga vitja {0}.", + "Share": "Býta", + "ShowIndicatorsFor": "Vís indikator fyri", + "ShowLess": "Vís minni", + "ShowMore": "Vís meiri", + "ShowParentImages": "Vís røð myndir", + "Shuffle": "Blanda", + "Shows": "Røðir", + "ShowTitle": "Vís heitið", + "Small": "Lítið", + "SmallCaps": "Lítlir stavir", + "Smaller": "Minni", + "Songs": "Sangir", + "Sort": "Skipa", + "SpecialFeatures": "Serstøk eyðkenni", + "StopRecording": "Steðga upptøku", + "Studio": "Filmsfelag", + "Studios": "Filmsfeløg", + "Subtitle": "Undirtekstur", + "SubtitleCyan": "Blágrønur", + "SubtitleGreen": "Grønt", + "SubtitleOffset": "Undirtekstur offset", + "SubtitleRed": "Reytt", + "Subtitles": "Undirtekstur", + "Suggestions": "Uppskot", + "Sync": "Synkronisera", + "SyncPlayGroupDefaultTitle": "{0}'sa bólkur", + "TabAdvanced": "Framkomin", + "TabCatalog": "Skrá", + "TabDashboard": "Kunningarbretti", + "TabDirectPlay": "Beinleiðis avspæling", + "TabLatest": "Lagt afturat fyri stuttum", + "TabLogs": "Gerðalistar", + "TabNetworking": "Net", + "TabNfoSettings": "NFO stillingar", + "TabPlugins": "Ískoytisforrit", + "TabProfiles": "Vangamyndir", + "TabRepositories": "Goymslur", + "TabServer": "Ambætari", + "TabStreaming": "Stroyming", + "TabUpcoming": "Komandi", + "Tags": "Frámerkir", + "TellUsAboutYourself": "Fortel um teg sjálvan", + "ThemeSongs": "Tema sangir", + "ThemeVideos": "Tema sjónbond", + "ThumbCard": "Tummlakort", + "TitleHardwareAcceleration": "Tólbúnað ferðøking", + "TitleHostingSettings": "Hýsingastillingar", + "TrackCount": "{0} spor", + "Trailers": "Forfilmar", + "Tuesday": "Týsdag", + "TV": "Sjónvarp", + "TypeOptionPluralAudio": "Ljóð", + "TypeOptionPluralBoxSet": "Boks sett", + "TypeOptionPluralMusicAlbum": "Tónleika album", + "TypeOptionPluralMusicVideo": "Tónleikasjónbond", + "TypeOptionPluralSeason": "Sesongir", + "TypeOptionPluralVideo": "Sjónbond", + "Typewriter": "Skrivimaskina", + "Uniform": "Einsháttað", + "Unmute": "Skrúva ljóðið upp", + "Unrated": "Eingin meting", + "Up": "Upp", + "ValueAudioCodec": "Ljóð codec: {0}", + "ValueCodec": "Codec: {0}", + "ValueContainer": "Bingja: {0}", + "ValueEpisodeCount": "{0} partar", + "ValueMinutes": "{0} min", + "ValueMovieCount": "{0} filmar", + "ValueOneEpisode": "1 partur", + "ValueOneMovie": "1 filmur", + "ValueOneMusicVideo": "1 tónleika sjónband", + "ValueOneSeries": "1 røð", + "ValueSeconds": "{0} sekund", + "ValueSeriesCount": "{0} røðir", + "Sports": "Ítróttur", + "Smart": "Smart", + "ShowYear": "Vís árið", + "SimultaneousConnectionLimitHelp": "Mest loyvda antalið av samstundis stroymingum. Skriva 0 fyri einki hámark.", + "Settings": "Stillingar", + "SettingsSaved": "Stillingar eru goymdar.", + "SelectAdminUsername": "Vinarliga vel eitt brúkaranavn til fyrisitara kontu.", + "Series": "Røð", + "SeriesCancelled": "Røðin er steðga.", + "Search": "Leita", + "SearchForCollectionInternetMetadata": "Leita eftir list og metadata á alnótini", + "SeriesSettings": "Røð stillingar", + "SeriesYearToPresent": "{0} - Núverandi", + "ServerRestartNeededAfterPluginInstall": "Jellyfin má endurbyrjast, aftaná at eitt ískoytisforrit er lagt inn.", + "ShowAdvancedSettings": "Vís framkomnar stillingar", + "StopPlayback": "Steðga avspæling", + "SubtitleGray": "Grátt", + "SubtitleLightGray": "Ljósagráður", + "SubtitleMagenta": "Viólreyður", + "AllowedRemoteAddressesHelp": "Komma býttur listið av IP addressum ella IP/netmask inngangur fyri netverk, ið verða loyvd at fjarbinda. Um hesin teigur er blankur, so eru allar fjar addressur loyvdar.", + "SubtitleWhite": "Hvítt", + "Arranger": "Fyrireikari", + "SubtitleYellow": "Gult", + "AskAdminToCreateLibrary": "Bið ein fyrisitari upprætta eitt savn.", + "TabAccess": "Atgongd", + "TypeOptionPluralEpisode": "Partar", + "TabContainers": "Kassar", + "TitlePlayback": "Avspæling", + "TabMusic": "Tónleikur", + "TabOther": "Annað", + "TagsValue": "Frámerkir: {0}", + "TabMyPlugins": "Míni ískoytisforrit", + "TabParentalControl": "Foreldra ræði", + "Sunday": "Sunnudag", + "SubtitleBlack": "Svart", + "TabResponses": "Svar", + "SubtitleBlue": "Blátt", + "TextSent": "Tekst sent.", + "TheseSettingsAffectSubtitlesOnThisDevice": "Hesir stillingar ávirka undirteksir á hesari eindini", + "Thumb": "Tummil", + "Thursday": "Hósdagur", + "Track": "Spor", + "Transcoding": "Umkodning", + "TypeOptionPluralBook": "Bøkur", + "TypeOptionPluralMovie": "Filmar", + "TypeOptionPluralMusicArtist": "Tónleikarar", + "TypeOptionPluralSeries": "Sjónvarpsrøðir", + "UnknownError": "Ein ókendur feilur hendi.", + "Unplayed": "Ikki spældur", + "Upload": "Send upp", + "UseEpisodeImagesInNextUp": "Brúka partamyndir í 'Komandi' og 'Hyggj víðari' teigum", + "UserMenu": "Brúkara valmynd", + "ValueAlbumCount": "{0} album", + "ValueConditions": "Treytir: {0}", + "ValueDiscNumber": "Fløga {0}", + "ValueMusicVideoCount": "{0} tónleika sjónbond", + "ValueOneAlbum": "1 album", + "ValueOneSong": "1 sangur", + "ValueSongCount": "{0} sangir" +} diff --git a/src/strings/fr.json b/src/strings/fr.json index ea1d558809..242cdd9d73 100644 --- a/src/strings/fr.json +++ b/src/strings/fr.json @@ -654,7 +654,7 @@ "LabelSaveLocalMetadata": "Enregistrer les illustrations dans les dossiers des médias", "LabelSaveLocalMetadataHelp": "L'enregistrement des illustrations dans les dossiers des médias les placera à un endroit où elles seront facilement modifiables.", "LabelScheduledTaskLastRan": "Dernière exécution {0}, durée {1}.", - "LabelScreensaver": "Économiseur d'écran", + "LabelScreensaver": "Écran de veille", "LabelSeasonNumber": "Numéro de saison", "LabelSelectFolderGroups": "Grouper automatiquement le contenu des dossiers suivants par catégories telles que 'Films', 'Musiques' et 'Séries TV'", "LabelSelectFolderGroupsHelp": "Les dossiers non cochés seront affichés individuellement dans leur vue propre.", @@ -1409,7 +1409,7 @@ "QuickConnectDescription": "Pour utiliser la connexion rapide, appuyez sur le bouton 'Connexion rapide' de l'appareil à connecter et entrez le code affiché ci-dessous.", "QuickConnectDeactivated": "La connexion rapide a été désactivée avant que la requête ne puisse être approuvée", "QuickConnectAuthorizeFail": "Code de connexion rapide inconnu", - "QuickConnectAuthorizeSuccess": "Requête autorisée", + "QuickConnectAuthorizeSuccess": "Vous avez authentifié votre appareil avec succès !", "QuickConnectAuthorizeCode": "Saisir le code {0} pour se connecter", "QuickConnectActivationSuccessful": "Activé avec succès", "QuickConnect": "Connexion rapide", @@ -1733,7 +1733,7 @@ "EnableAudioNormalizationHelp": "La normalisation audio ajoutera un gain constant pour maintenir la moyenne au niveau souhaité (-18dB).", "EnableAudioNormalization": "Normalisation audio", "LabelEnableLUFSScan": "Activer l’analyse LUFS", - "LabelEnableLUFSScanHelp": "Activer l’analyse LUFS pour la musique (cela prendra plus de temps et de ressources).", + "LabelEnableLUFSScanHelp": "Les clients peuvent normaliser la lecture audio pour obtenir une intensité sonore égale sur toutes les pistes. L'analyse de bibliothèque nécessitera alors plus de temps et de ressources.", "GetThePlugin": "Obtenir l'extension", "Notifications": "Notifications", "NotificationsMovedMessage": "La fonctionnalité de notifications a été transférée à l'extension Webhook.", @@ -1748,8 +1748,8 @@ "LabelLevel": "Niveau", "LabelMediaDetails": "Détails du média", "LabelSystem": "Système", - "LogLevel.Trace": "Suivi", - "LogLevel.Debug": "Debug", + "LogLevel.Trace": "Trace", + "LogLevel.Debug": "Débogage", "LogLevel.Information": "Information", "LogLevel.Warning": "Avertissement", "LogLevel.Error": "Erreur", @@ -1762,7 +1762,19 @@ "LabelThrottleDelaySecondsHelp": "Durée en secondes après laquelle le débit du transcodage sera ajusté. Doit être suffisamment grande pour que le client puisse conserver une mémoire tampon saine. Ne fonctionne que si l'adaptation de la vitesse de transcodage est activée.", "LabelSegmentKeepSeconds": "Durée de conservation des segments", "LabelSegmentKeepSecondsHelp": "Durée en secondes de conservation des segments avant écrasement. La valeur doit être supérieure au délai d'ajustement. Ne fonctionne que si la suppression des segments est activée.", - "LabelBackdropScreensaverIntervalHelp": "Le temps en secondes entre différents fonds d'écran lors de l'utilisation de l'économiseur d'écran à fonds d'écran.", - "LabelBackdropScreensaverInterval": "Intervalle de l'économiseur d'écran à fonds d'écran", - "AllowAv1Encoding": "Autoriser l'encodage au format AV1" + "LabelBackdropScreensaverIntervalHelp": "Temps en secondes entre chaque fond d'écran de l'écran de veille Diaporama.", + "LabelBackdropScreensaverInterval": "Intervalle de l'écran de veille Diaporama", + "AllowAv1Encoding": "Autoriser l'encodage au format AV1", + "GoHome": "Retour à l'accueil", + "ListView": "Vue en liste", + "GridView": "Vue en grille", + "BackdropScreensaver": "Écran de veille Diaporama", + "LogoScreensaver": "Écran de veille Logo", + "UnknownError": "Une erreur inconnue s’est produite.", + "MachineTranslated": "Traduction automatique", + "AiTranslated": "Traduction par IA", + "ForeignPartsOnly": "Parties forcées/en langues étrangères uniquement", + "HearingImpairedShort": "HI/SDH", + "HeaderGuestCast": "Invités vedettes", + "LabelIsHearingImpaired": "Sous-titrage pour sourds et malentendants" } diff --git a/src/strings/he.json b/src/strings/he.json index 28ed1c274e..089bbe949b 100644 --- a/src/strings/he.json +++ b/src/strings/he.json @@ -853,7 +853,7 @@ "EnableBlurHashHelp": "תמונות שעדיין נטענות יוצגו עם מציין מיקום ייחודי.", "Bwdif": "BWDIF", "ButtonCast": "שדר למכשיר", - "AllowTonemappingHelp": "מיפוי-טונים מאפשר המרה של וידאו מ-HDR ל-SDR תוך שמירה על פרטי וצבעי תמונה, החשובים לשימור מידע מהסצנה המקורית. כרגע עובד רק בקידוד קבצים של HDR10 או HLG. תכונה זו מצריכה הרצה של OpenCL או CUDA בהתאם.", + "AllowTonemappingHelp": "מיפוי-טונים מאפשר המרה של וידאו מ-HDR ל-SDR תוך שמירה על פרטי וצבעי תמונה, החשובים לשימור מידע מהסצנה המקורית. כרגע עובד רק בקידוד קבצים של HDR10, HLG ו-DoVi. תכונה זו מצריכה הרצה של OpenCL או CUDA בהתאם.", "Subtitle": "כתובית", "StopRecording": "הפסק הקלטה", "SortByValue": "מיין לפי {0}", @@ -1181,5 +1181,26 @@ "AllowSegmentDeletionHelp": "מחק אותות ישנים אחרי שהם נשלחו ללקוח. זה ימנע שמירת כל הקובץ המפוענח על הדיסק. עובד רק כאשר מצב חנק פעיל. כבה אפשרות זו אם הינך חווה בעיות בנגן.", "LabelThrottleDelaySeconds": "חנוק אחרי", "LabelSegmentKeepSeconds": "זמן שמירת אותות", - "LabelSegmentKeepSecondsHelp": "זמן בשניות שבהן הקטעים צריכים להישאר לפני שהם נדרסים. צריך להיות גדול מ\"חנוק אחרי\". עובד רק כאשר מחיקת אות מאופשרת." + "LabelSegmentKeepSecondsHelp": "זמן בשניות שבהן הקטעים צריכים להישאר לפני שהם נדרסים. צריך להיות גדול מ\"חנוק אחרי\". עובד רק כאשר מחיקת אות מאופשרת.", + "GoHome": "מעבר למסך הבית", + "LabelEnableDlnaClientDiscoveryIntervalHelp": "קובע את משך הזמן בשניות בין שני חיפושי SSDP.", + "LabelDropSubtitleHere": "גרור קובץ כתוביות לכאן, או לחץ לבחירת קובץ.", + "LabelEmbedAlbumArtDidlHelp": "שיטה זו למציאת תמונת אלבום עדיפה עבור מכשירים מסויימים. במכשירים אחרים ייתכן שהניגון ייכשל.", + "GridView": "תצוגת רשת", + "EnableAudioNormalizationHelp": "נירמול יגביר את עוצמת השמע כדי לשמר ממוצע רצוי (-18 דציבלים).", + "LabelEnableAudioVbr": "אפשר קידוד שמע בקצב משתנה (VBR)", + "LabelDummyChapterDuration": "פרק זמן", + "LabelEmbedAlbumArtDidl": "הטמע תמונת אלבום ב-DIDL", + "LabelEnableAudioVbrHelp": "קידוד בקצב משתנה מציע איכות טובה יותר בהשוואה לקצב ממוצע, אבל במקרים נדירים עלול לגרום לבעיות טעינה (buffering) ותאימות.", + "LabelEnableDlnaPlayToHelp": "מצא מכשירים ברשת שלך ואפשר שליטה מרחוק בהם.", + "LabelCreateHttpPortMapHelp": "אפשר מיפוי פורטים אוטומטי כדי לייצר כלל עבור תעבורת HTTP, בנוסף לתעבורת HTTPS.", + "LabelChapterImageResolutionHelp": "רזולוציה של תמונות הפרקים שיופקו. שינוי הערך לא ישפיע על פרקי דמה קיימים.", + "LabelKodiMetadataEnablePathSubstitution": "אפשר החלפות בשדה path", + "LabelH264Crf": "CRF של קידוד H.264", + "LabelHardwareAccelerationTypeHelp": "האצת חומרה דורשת הגדרות נוספות.", + "LabelH265Crf": "CRF של קידוד H.265", + "LabelKodiMetadataEnableExtraThumbsHelp": "תמונות שהורדו יכולות להישמר לשדות extrafanart ו-extrathumbs בו זמנית לצורך התאמה מירבית לסקינים של קודי.", + "LabelKodiMetadataEnableExtraThumbs": "העתק extrafanart לשדה extrathumbs", + "LabelGroupMoviesIntoCollectionsHelp": "כל הסרטים באוסף יופיעו בתור פריט מקובץ אחד ברשימות סרטים.", + "LabelHomeScreenSectionValue": "איזור {0} בעמוד הבית" } diff --git a/src/strings/it.json b/src/strings/it.json index 076aba22b7..49aa147f18 100644 --- a/src/strings/it.json +++ b/src/strings/it.json @@ -1390,7 +1390,7 @@ "QuickConnectInvalidCode": "Codice Quick Connect non valido", "QuickConnectDescription": "Per registrarsi usando Quick Connect, seleziona il pulsante Quick Connect nel dispositivo che stai usando per accedere ed inserisci il codice sottostante.", "QuickConnectDeactivated": "Quick Connect è stato disattivato prima che la richiesta di login venisse approvata", - "QuickConnectAuthorizeSuccess": "Richiesta autorizzata", + "QuickConnectAuthorizeSuccess": "Device autenticato con successo!", "QuickConnectActivationSuccessful": "Attivato con successo", "QuickConnect": "Connessione Rapida", "LabelQuickConnectCode": "Codice Quick Connect", @@ -1741,7 +1741,7 @@ "HeaderConfirmRepositoryInstallation": "Conferma dell'installazione del repository dei plugin", "LabelDeveloper": "Sviluppatore", "LabelEnableLUFSScan": "Abilita LUFS scan", - "LabelEnableLUFSScanHelp": "Abilita LUFS scan per la musica (Impiegherà più tempo e più risorse).", + "LabelEnableLUFSScanHelp": "I client possono normalizzare la riproduzione dell'audio per avere un uguale volume tra le tracce. Ciò impiegherà più tempo durante lo scan delle librerie e più risorse.", "MessageRepositoryInstallDisclaimer": "ATTENZIONE: L'installazione di repository di plugin di terze parti può portare dei rischi. Può contenere codice instabile o malevolo e può cambiare in qualsiasi momento. Installa solo plugin degli autori di cui ti fidi.", "Unknown": "Sconosciuto", "LabelDate": "Data", @@ -1761,5 +1761,8 @@ "LabelThrottleDelaySeconds": "Throttle dopo", "LabelThrottleDelaySecondsHelp": "Tempo in secondi dopo cui il transcodificatore sarà messo in throttle. Deve essere sufficientemente grande perché il client mantenga un buon buffer. Funziona solo se il throttling è abilitato.", "LabelSegmentKeepSeconds": "Il tempo per cui tenere i segmenti", - "LabelSegmentKeepSecondsHelp": "Tempo in secondi per cui i segmenti saranno tenuti prima di essere sovrascritti. Deve essere più grande di \"Throttle dopo\". Funziona solo se l'eliminazione dei segmenti è abilitata." + "LabelSegmentKeepSecondsHelp": "Tempo in secondi per cui i segmenti saranno tenuti prima di essere sovrascritti. Deve essere più grande di \"Throttle dopo\". Funziona solo se l'eliminazione dei segmenti è abilitata.", + "AllowAv1Encoding": "Permetti la codifica nel formato AV1", + "GoHome": "Vai alla Home", + "GridView": "Vista Griglia" } diff --git a/src/strings/ja.json b/src/strings/ja.json index 0e9537755e..ff0acbdb1e 100644 --- a/src/strings/ja.json +++ b/src/strings/ja.json @@ -1758,5 +1758,11 @@ "LogLevel.Error": "エラー", "LogLevel.Critical": "致命的なエラー", "MessageRepositoryInstallDisclaimer": "【警告】コミュニティメンバーによって作成されたプラグインはリスクを伴います。不安定、または悪質な挙動、予期せぬ時に変更されるなどの可能性があります。信用性の高い開発者からのプラグインのみをインストールすることをお勧めします。", - "Select": "選択" + "Select": "選択", + "AllowAv1Encoding": "AV1フォーマットのエンコードを許可する", + "GoHome": "ホームへ", + "UnknownError": "未知のエラーが発生しました。", + "LogoScreensaver": "ロゴスクリーンセーバー", + "AiTranslated": "AI翻訳", + "MachineTranslated": "機械翻訳" } diff --git a/src/strings/kn.json b/src/strings/kn.json index 4766f78b38..2162b1e314 100644 --- a/src/strings/kn.json +++ b/src/strings/kn.json @@ -7,5 +7,24 @@ "LanNetworksHelp": "ಬ್ಯಾಂಡ್‌ವಿಡ್ತ್ ನಿರ್ಬಂಧಗಳನ್ನು ಜಾರಿಗೊಳಿಸುವಾಗ ಸ್ಥಳೀಯ ನೆಟ್‌ವರ್ಕ್‌ನಲ್ಲಿ ಪರಿಗಣಿಸಲಾಗುವ ನೆಟ್‌ವರ್ಕ್‌ಗಳಿಗಾಗಿ IP ವಿಳಾಸಗಳ ಅಲ್ಪವಿರಾಮದಿಂದ ಬೇರ್ಪಡಿಸಿದ ಪಟ್ಟಿ ಅಥವಾ IP/ನೆಟ್‌ಮಾಸ್ಕ್ ನಮೂದುಗಳು. ಹೊಂದಿಸಿದರೆ, ಎಲ್ಲಾ ಇತರ IP ವಿಳಾಸಗಳನ್ನು ಬಾಹ್ಯ ನೆಟ್‌ವರ್ಕ್‌ನಲ್ಲಿ ಎಂದು ಪರಿಗಣಿಸಲಾಗುತ್ತದೆ ಮತ್ತು ಬಾಹ್ಯ ಬ್ಯಾಂಡ್‌ವಿಡ್ತ್ ನಿರ್ಬಂಧಗಳಿಗೆ ಒಳಪಟ್ಟಿರುತ್ತದೆ. ಖಾಲಿ ಬಿಟ್ಟರೆ, ಸರ್ವರ್‌ನ ಸಬ್‌ನೆಟ್ ಮಾತ್ರ ಸ್ಥಳೀಯ ನೆಟ್‌ವರ್ಕ್‌ನಲ್ಲಿದೆ ಎಂದು ಪರಿಗಣಿಸಲಾಗುತ್ತದೆ.", "Actor": "ನಟ", "AccessRestrictedTryAgainLater": "ಪ್ರಸ್ತುತ ಪ್ರವೇಶವನ್ನು ನಿರ್ಬಂಧಿಸಲಾಗಿದೆ. ದಯವಿಟ್ಟು ನಂತರ ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ.", - "Absolute": "ಸಂಪೂರ್ಣ" + "Absolute": "ಸಂಪೂರ್ಣ", + "Default": "ಪೂರ್ವನಿಯೋಜಿತ", + "Channels": "ಮೂಲಗಳು", + "Folders": "ಫೋಲ್ಡರ್‌ಗಳು", + "HeaderContinueWatching": "ನೋಡುವುದನ್ನು ಮುಂದುವರಿಸಿ", + "Playlists": "ಪ್ಲೇಪಟ್ಟಿಗಳು", + "Songs": "ಹಾಡುಗಳು", + "Movies": "ಚಲನಚಿತ್ರಗಳು", + "MusicVideos": "ಸಂಗೀತ ವೀಡಿಯೊಗಳು", + "Sync": "ಹೊಂದಿಕೆ", + "ValueSpecialEpisodeName": "ವಿಶೇಷ - {0}", + "Albums": "ಸಂಪುಟ", + "Genres": "ಪ್ರಕಾರಗಳು", + "Favorites": "ಮೆಚ್ಚಿನವುಗಳು", + "HeaderAlbumArtists": "ಸಂಪುಟ ಕಲಾವಿದರು", + "Collections": "ಸಂಗ್ರಹಣೆಗಳು", + "Shows": "ಧಾರವಾಹಿಗಳು", + "Photos": "ಚಿತ್ರಗಳು", + "Artists": "ಕಲಾವಿದರು", + "Books": "ಪುಸ್ತಕಗಳು" } diff --git a/src/strings/ko.json b/src/strings/ko.json index 404cb7150f..798c0faab3 100644 --- a/src/strings/ko.json +++ b/src/strings/ko.json @@ -1219,7 +1219,7 @@ "QuickConnectDescription": "퀵커넥트로 로그인하려면 로그인중인 장치에서 퀵커넥트 버튼을 선택하고 아래 표시된 코드를 입력하십시오.", "QuickConnectDeactivated": "로그인 요청이 승인되기 전에 Quick connect가 비활성화되었습니다", "QuickConnectAuthorizeFail": "알수없는 퀵커넥트 코드", - "QuickConnectAuthorizeSuccess": "승인 요청", + "QuickConnectAuthorizeSuccess": "기기가 성공적으로 인증되었습니다!", "QuickConnectAuthorizeCode": "로그인하려면 {0} 코드를 입력하세요", "QuickConnectActivationSuccessful": "성공적으로 활성화되었습니다", "QuickConnect": "퀵커넥트", @@ -1349,7 +1349,7 @@ "ResumeAt": "{0}에서 재생", "MessageChangeRecordingPath": "녹음 폴더를 변경해도 기존 녹음은 이전 위치에서 새 위치로 마이그레이션되지 않습니다. 원하는 경우 수동으로 이동해야합니다.", "Premieres": "첫날", - "AllowTonemappingHelp": "톤 매핑은 원본 장면을 보여주는데 매우 중요한 정보인 이미지 디테일과 색을 유지하면서 HDR에서 SDR로 비디오의 다이나믹 레인지를 변환할 수 있습니다. 현재 10비트 HDR10, HLG, DoVi가 지원되는 비디오에만 작동합니다. 이 기능은 해당되는 OpenCL이나 CUDA 런타임이 필요합니다.", + "AllowTonemappingHelp": "톤 매핑은 원본 장면을 재현하는데 매우 중요한 정보인 이미지 디테일과 색을 유지하면서 HDR에서 SDR로 비디오의 다이나믹 레인지를 변환할 수 있습니다. 현재 10비트 HDR10, HLG, DoVi가 지원되는 비디오에만 작동합니다. 이 기능은 해당되는 OpenCL이나 CUDA 런타임이 필요합니다.", "EnableTonemapping": "톤 매핑 활성화", "LabelOpenclDeviceHelp": "이것은 톤 매핑에 사용되는 OpenCL 장치입니다. 점의 왼쪽은 플랫폼 번호이고 오른쪽은 플랫폼의 장치 번호입니다. 기본값은 0.0입니다. OpenCL 하드웨어 가속 방법이 포함 된 ffmpeg 응용프로그램 파일이 필요합니다.", "LabelOpenclDevice": "OpenCL 장치", @@ -1691,7 +1691,7 @@ "LabelHardwareEncodingOptions": "하드웨어 인코딩 옵션", "VideoBitrateNotSupported": "비디오의 비트레이트가 지원되지 않습니다", "LabelEnableLUFSScan": "LUFS 분석 활성화", - "LabelEnableLUFSScanHelp": "음악에 대한 LUFS 분석 활성화 (더 많은 시간과 리소스 사용).", + "LabelEnableLUFSScanHelp": "클라이언트가 트랙들이 일정한 음량을 갖도록 오디오 트랙을 평준화할 수 있게 합니다. 라이브러리 스캔에 더 많은 시간과 리소스가 사용됩니다.", "MessageRenameMediaFolder": "미디어 라이브러리의 이름을 바꾸면 모든 메타데이터가 손실되므로 주의하여 진행하세요.", "PreferEmbeddedExtrasTitlesOverFileNames": "엑스트라의 경우 파일 이름보다 내장된 제목을 선호", "ReleaseGroup": "발매 그룹", @@ -1758,5 +1758,20 @@ "SubtitleWhite": "하양", "UnknownAudioStreamInfo": "오디오 스트림 정보를 알 수 없습니다", "MediaInfoBlPresentFlag": "DV bl 프리셋 플래그", - "TonemappingModeHelp": "톤 매핑 모드를 선택하세요. 하이라이트 부분이 날아가는 경우 RGB 모드로 전환해 보세요." + "TonemappingModeHelp": "톤 매핑 모드를 선택하세요. 하이라이트 부분이 날아가는 경우 RGB 모드로 전환해 보세요.", + "LabelIsHearingImpaired": "청각 장애인용 (SDH)", + "AllowAv1Encoding": "AV1 포맷으로 인코딩 허용", + "GoHome": "홈으로", + "GridView": "그리드 뷰", + "LabelBackdropScreensaverInterval": "배경 화면보호기 간격", + "LabelBackdropScreensaverIntervalHelp": "배경 화면보호기를 사용할 때 서로 다른 배경으로 바뀌는 간격 시간(초)", + "ListView": "리스트 뷰", + "BackdropScreensaver": "배경 화면보호기", + "LogoScreensaver": "로고 화면보호기", + "UnknownError": "알 수 없는 에러가 발생하였습니다.", + "AiTranslated": "AI 번역", + "MachineTranslated": "기계 번역", + "ForeignPartsOnly": "강제/외부 파트만", + "HearingImpairedShort": "청각장애/SDH", + "HeaderGuestCast": "게스트" } diff --git a/src/strings/lt-lt.json b/src/strings/lt-lt.json index 16694a71ed..5a00e5fba0 100644 --- a/src/strings/lt-lt.json +++ b/src/strings/lt-lt.json @@ -1162,5 +1162,7 @@ "LabelMaxVideoResolution": "Maksimali leistina video transkodavimo resoliucija", "LabelEnableAudioVbrHelp": "Kintama bitų sparta siūlo geresnę kokybę lyginant su vidutine bitų sparta, bet retais atvejais gali sukelti krovimo ir palaikymo problemas.", "LabelKodiMetadataEnablePathSubstitutionHelp": "Įjungti kelio pakeitimą nuotraukoms naudojant serverio kelio pakeitimo nustatymus.", - "LabelKodiMetadataDateFormatHelp": "Visos datos iš NFO failų bus ištraukiamos šiuo formatu." + "LabelKodiMetadataDateFormatHelp": "Visos datos iš NFO failų bus ištraukiamos šiuo formatu.", + "AllowSegmentDeletion": "Ištrinti segmentus", + "AllowSegmentDeletionHelp": "Ištrinkite senus segmentus, kai jie buvo išsiųsti klientui. Taip išvengiama viso perkoduoto failo saugojimo diske. Veiks tik su įjungtu droseliu. Išjunkite tai, jei kyla atkūrimo problemų." } diff --git a/src/strings/lv.json b/src/strings/lv.json index 5490a7aed3..03a6911e18 100644 --- a/src/strings/lv.json +++ b/src/strings/lv.json @@ -643,7 +643,7 @@ "Recordings": "Ieraksti", "RecordingCancelled": "Ieraksts atcelts.", "RecordSeries": "Ierakstīt sēriju", - "Record": "Ierakstīt", + "Record": "Ieraksts", "RecommendationBecauseYouWatched": "Tāpēc ka tu skatījies {0}", "RecommendationBecauseYouLike": "Tāpēc ka tev patīk {0}", "RecentlyWatched": "Nesen skatīts", @@ -887,7 +887,7 @@ "MediaInfoAspectRatio": "Attēla proporcijas", "MaxParentalRatingHelp": "Saturs ar augstāku reitingu tiks paslēpts no šī lietotāja.", "LibraryAccessHelp": "Izvēlies bibliotēkas, ko koplietot ar šo lietotāju. Administratori spēs rediģēt visas bibliotēkas izmantojot metadatu pārvaldnieku.", - "LearnHowYouCanContribute": "Uzziniet, kā jūs varat dot ieguldījumu.", + "LearnHowYouCanContribute": "Uzziniet, kā jūs varat palīdzēt.", "LabelUserLoginAttemptsBeforeLockout": "Neizdevušies piekļuves mēģinājumi pirms lietotājs tiek bloķēts", "LabelTranscodingThreadCount": "Pārkodēšanas pavedienu daudzums", "LabelTranscodes": "Transkodi", @@ -1228,7 +1228,7 @@ "ErrorPlayerNotFound": "Atskaņotājs pieprasītajam mēdijam nav atrasts.", "ErrorAddingListingsToSchedulesDirect": "Pievienojot sarakstu jūsu Schedules Direct kontam. Schedules Direct atļauj vienā kontā tikai ierobežotu grupu skaitu. Pirms turpināt, jums būs jāpiesakās Schedules Direct vietnē un jānoņem citi ieraksti no sava konta.", "Engineer": "Skaņas inženieris", - "DirectPlayHelp": "Avota fails ir pilnībā saderīgs ar šo klientu, un šī sesija saņem failu bez modifikācijas.", + "DirectPlayHelp": "Oriģinālais fails ir pilnībā saderīgs ar šo klientu un šī sesija saņem failu bez modifikācijas.", "Cursive": "Kursīvs", "Console": "Konsole", "Conductor": "Diriģents", @@ -1302,7 +1302,7 @@ "AllowEmbeddedSubtitlesAllowTextOption": "Atļaut tekstu", "OptionResElement": "'res' elements", "QuickConnectAuthorizeCode": "Lai autorizētos, ievadiet kodu {0}", - "QuickConnectAuthorizeSuccess": "Pieprasījums autorizēts", + "QuickConnectAuthorizeSuccess": "Jūsu ierīce ir autorizēta!", "MediaInfoTitle": "Nosaukums", "MillisecondsUnit": "ms", "Photo": "Attēls", @@ -1363,7 +1363,7 @@ "EnableAudioNormalizationHelp": "Audio normalizācija pievienos konstantu skaļuma pastiprinājumu, lai saglabātu vidējo skaļumu vēlamajā līmenī (-18dB).", "EnableAudioNormalization": "Audio Normalizācija", "LabelEnableLUFSScan": "Iespējot LUFS skenēšanu", - "LabelEnableLUFSScanHelp": "Iespējot LUFS skenēšanu mūzikai (Tas aizņems vairāk laika un resursu).", + "LabelEnableLUFSScanHelp": "Klienti var normalizēt audio atskaņošanu, lai iegūtu vienādu skaļumu visos ierakstos. (Tas pataisīs bibliotēkas skenēšanu lēnāku un patērēs vairāk resursus).", "MessageNoItemsAvailable": "Pašlaik nav pieejams neviens vienums.", "MessageNoCollectionsAvailable": "Kolekcijas ļauj izmantot personalizētas filmu, seriālu un albumu grupas. Noklikšķiniet uz pogas \"+\", lai sāktu veidot kolekcijas.", "MessageNoRepositories": "Nav repozitoriju.", @@ -1377,8 +1377,8 @@ "MessageNoFavoritesAvailable": "Pašlaik nav pieejami nekādi favorīti.", "MessageImageTypeNotSelected": "Lūdzu, izvēlaties attēla veidu no nolaižamās izvēlnes.", "MessageLeaveEmptyToInherit": "Atstājiet tukšu, lai mantotu iestatījumus no vecākā elementa vai globālās noklusējuma vērtības.", - "MessagePluginInstallDisclaimer": "Kopienas dalībnieku izveidoti spraudņi ir lielisks veids, kā uzlabot savu pieredzi, izmantojot papildu funkcijas un priekšrocības. Pirms instalēšanas ņemiet vērā to iespējamo ietekmi uz jūsu serveri, piemēram, ilgāku bibliotēkas skenēšanu, papildu fona apstrādi un sistēmas stabilitātes samazināšanos.", - "MessagePasswordResetForUsers": "Šādu lietotāju paroles ir atiestatītas. Tagad viņi var pierakstīties, izmantojot Easy PIN kodus, kas tika izmantoti atiestatīšanai.", + "MessagePluginInstallDisclaimer": "Kopienas dalībnieku izveidoti spraudņi ir lielisks veids, kā uzlabot savu pieredzi, izmantojot papildu funkcijas un priekšrocības, bet šādi spraudņi var saturēt bīstamu vai nestabilu kodu. Pirms instalēšanas ņemiet vērā to iespējamo ietekmi uz jūsu serveri, piemēram, ilgāku bibliotēkas skenēšanu, papildu fona apstrādi un sistēmas stabilitātes samazināšanos.", + "MessagePasswordResetForUsers": "Šo lietotāju paroles ir atiestatītas. Tagad viņi var pierakstīties, izmantojot PIN kodus, kas tika izmantoti atiestatīšanai.", "MessagePluginInstalled": "Spraudnis tika veiksmīgi instalēts. Lai izmaiņas stātos spēkā, serveris ir jārestartē.", "HeaderRecordingMetadataSaving": "Metadatu Ierakstīšana", "AllowCollectionManagement": "Ļaut konkrētajam lietotājam pārvaldīt kolekciju", @@ -1480,5 +1480,42 @@ "QuickConnectInvalidCode": "Nederīgs Quick Connect kods", "OptionAutomaticallyGroupSeries": "Automātiski apvienot sērijas, kuras izvietotas vairākās mapēs", "PreferEmbeddedTitlesOverFileNamesHelp": "Noteikt redzamo nosaukumu, kas jāizmanto, ja nav pieejami interneta metadati vai vietējie metadati.", - "OptionSpecialEpisode": "Speciālizlaidumi" + "OptionSpecialEpisode": "Speciālizlaidumi", + "AllowSegmentDeletion": "Dzēst segmentus", + "AllowSegmentDeletionHelp": "Dzēst vecus segmentus pēc to nosūtīšanas klientam. Šis lauj neglabāt visu pārkodēto failu diskā. Izslēdziet šo tikai ja jums ir problēmas ar atskanosanu.", + "GoHome": "Doties mājās", + "HeaderEpisodesStatus": "Epizožu statuss", + "LabelDate": "Datums", + "LabelBackdropScreensaverInterval": "Fona ekrānsaudzētāja intervāls", + "LabelBackdropScreensaverIntervalHelp": "Laiks sekundēs starp dažādiem foniem, ja tiek izmantots fona ekrānsaudzētājs.", + "GridView": "Tabulas skats", + "BackdropScreensaver": "Fona ekrānsaudzētājs", + "LabelLevel": "Līmenis", + "HeaderConfirmRepositoryInstallation": "Apstipriniet spraudņu krātuves instalāciju", + "LabelDeveloper": "Izstrādātājs", + "LabelThrottleDelaySeconds": "Bremzēt pēc", + "LabelThrottleDelaySecondsHelp": "Laiks sekundēs pēc kura pārkodētājs tiks pēc kura pārkodētājs tiks bremzēts. Šim jābūt pietiekami lielam lai klients saglabātu veselīgu buferi. Šis strādā tikai ja bremzēšana ir ieslēgta.", + "LabelSegmentKeepSeconds": "Cik ilgi paturēt segmentus", + "HeaderGuestCast": "Vieszvaigznes", + "RecommendationStarring": "Lomās {0}", + "LogLevel.Trace": "Izsekot", + "ShowTitle": "Parādīt virsrakstu", + "ShowYear": "Parādīt gadu", + "RecordingScheduled": "Ieraksts ieplānots.", + "LabelMediaDetails": "Multivides detaļas", + "LogLevel.Information": "Informācija", + "RefreshMetadata": "Atsvaidzināt metadatus", + "RefreshDialogHelp": "Metadati tiek atsvaidzināti balstoties uz iestatījumiem un interneta pakalpojumiem, kas ir iespējoti info panelī.", + "LabelSyncPlayNoGroups": "Neviena grupa nav pieejama", + "ListView": "Saraksta skats", + "LogLevel.Debug": "Atkļūdošana", + "LogLevel.Error": "Brīdinājums", + "LogoScreensaver": "Logo ekrānsaudzētājs", + "RecommendationDirectedBy": "Režisēja {0}", + "MessageRepositoryInstallDisclaimer": "BRĪDINĀJUMS: Trešās puses spraudņa instalācija ir riskanta. Tas var saturēt nestabilu vai bīstamu kodu un var mainīties jebkurā brīdī. Instalējiet tikai spraudņus no uzticamiem avotiem.", + "PleaseConfirmRepositoryInstallation": "Lūdzu spiediet OK lai apstiprināku ka esat izlasījuši brīdinājumu un vēlaties turpināt spraudņu krātuves instalāciju.", + "LogLevel.None": "Nekas", + "RefreshQueued": "Atsvaidzināšana ieplānota.", + "LabelSystem": "Sistēma", + "LogLevel.Critical": "Kritisks" } diff --git a/src/strings/ms.json b/src/strings/ms.json index 86ade162ab..6a000566e9 100644 --- a/src/strings/ms.json +++ b/src/strings/ms.json @@ -4,7 +4,7 @@ "LabelFinish": "Habis", "LabelYoureDone": "Kamu Selesai!", "ParentalRating": "Parental Rating", - "SettingsSaved": "Seting Disimpan.", + "SettingsSaved": "Tetapan Disimpan.", "Absolute": "Mutlak", "AccessRestrictedTryAgainLater": "Akses dihalang pada masa ini. Sila cuba sebentar lagi.", "Actor": "Pelakon", @@ -20,7 +20,7 @@ "AllLibraries": "Semua pustaka", "AllowMediaConversion": "Membolehkan penukaran media", "AllowMediaConversionHelp": "Memberi atau menolak akses penukaran ciri media.", - "Albums": "Albums", + "Albums": "Album", "Alerts": "Amaran", "AllChannels": "Semua saluran", "AllComplexFormats": "Semua format kompleks (ASS, SSA, VobSub, PGS, SUB, IDX, …)", @@ -246,5 +246,10 @@ "EnableAudioNormalizationHelp": "Normilasi audio akan meletakkan gain yang konstant agar purata di tahap yang dikehendaki (-18dB).", "Console": "konsol", "DeathDateValue": "Died: {0} , program error", - "AllowCollectionManagement": "Benarkan pengguna ini meguruskan koleksi" + "AllowCollectionManagement": "Benarkan pengguna ini meguruskan koleksi", + "AllowSegmentDeletion": "Padam segment", + "AllowSegmentDeletionHelp": "Padam segmen lama setelah ia dihantar ke pelayan. Ini menghalang file transcode disimpan dalam disk. Ia akan berfungsi dengan penghad haju dihidupkan. Matikan tetapan ini jika anda mengalami isu dengan pemain video.", + "LabelThrottleDelaySeconds": "Penghad laju setelah", + "Settings": "Tetapan", + "SelectServer": "Pilih pelayan" } diff --git a/src/strings/nb.json b/src/strings/nb.json index 6f7f1c7705..91f77b623c 100644 --- a/src/strings/nb.json +++ b/src/strings/nb.json @@ -1385,7 +1385,7 @@ "QuickConnectDescription": "Hvis du vil logge på med hurtigtilkobling velger du 'Hurtigtilkobling'-knappen på enheten du logger på fra, og skriver inn koden som vises nedenfor.", "QuickConnectDeactivated": "Hurtigtilkobling ble deaktivert før påloggingsforespørselen kunne godkjennes", "QuickConnectAuthorizeFail": "Ukjent hurtigtilkoblings-kode", - "QuickConnectAuthorizeSuccess": "Forespørsel autorisert", + "QuickConnectAuthorizeSuccess": "Du har autentisert enheten din!", "QuickConnectAuthorizeCode": "Skriv inn kode {0} for å logge inn", "QuickConnectActivationSuccessful": "Aktivert", "QuickConnect": "Hurtigtilkobling", @@ -1418,7 +1418,7 @@ "LabelTonemappingRange": "Tonekartlegging-område", "TonemappingAlgorithmHelp": "Tonekartlegging kan finjusteres. Hvis du ikke er kjent med disse alternativene er det bare å beholde standardinnstillingen. Anbefalt verdi er BT.2390.", "LabelTonemappingAlgorithm": "Velg algoritmen som skal brukes for tonekartlegging", - "AllowTonemappingHelp": "Tonekartlegging kan forvandle det dynamiske området på en video fra HDR til SDR samtidig som bildedetaljer og farger opprettholdes, noe som er veldig viktig informasjon for å representere den opprinnelige scenen. Fungerer for øyeblikket bare med HDR10-, HLG- og DoVi-videoer. Dette krever korresponderende OpenCL eller CUDA runtime.", + "AllowTonemappingHelp": "Tonekartlegging kan forvandle det dynamiske området på en video fra HDR til SDR samtidig som bildedetaljer og farger opprettholdes, noe som er veldig viktig informasjon for å representere den opprinnelige scenen. Fungerer for øyeblikket bare med HDR10, HLG og DoVi-videoer. Dette krever korresponderende OpenCL eller CUDA runtime.", "OptionMaxActiveSessionsHelp": "En verdi på 0 skrur av denne funksjonen.", "OptionMaxActiveSessions": "Sett maksimalt antall tilgjengelige brukerøkter.", "LabelUserMaxActiveSessions": "Maksimalt antall samtidige brukerøkter", @@ -1734,7 +1734,7 @@ "LabelParallelImageEncodingLimitHelp": "Høyeste antall bildeenkodinger som tillates å kjøre parallelt. Å sette denne til 0 vil velge en grensse basert på systemspesifikasjonene dine.", "LabelChapterImageResolutionHelp": "Oppløsningen til kapittelbildene. Enring av dette vil ikke ha noen effekt på eksisterende kapittelbilder.", "LabelParallelImageEncodingLimit": "Parallell bildeenkodingsgrense", - "LabelEnableLUFSScanHelp": "Slå på LUFS-skanning for musikk (dette vil ta lengre tid og mere ressurser).", + "LabelEnableLUFSScanHelp": "Klienter kan normalisere lydnivå for å få lik lydstyrke på tvers av spor. Dette vil gjøre bibliotekskanning lengre og bruke flere ressurser.", "ResolutionMatchSource": "Bruk kildetreff", "SaveRecordingNFOHelp": "Lagre metadata fra EPG-listekilde sammen med media.", "AllowCollectionManagement": "La denne brukeren organisere samlinger", @@ -1760,5 +1760,20 @@ "LabelThrottleDelaySecondsHelp": "Tid i sekunder hvoretter transkoderen vil bli begrenset. Må være stor nok til at klienten kan opprettholde en sunn buffer. Funker kun om begrensning er påskrudd.", "LabelSegmentKeepSeconds": "Tid å beholde segmenter", "LabelSegmentKeepSecondsHelp": "Tid i sekunder som segmenter skal beholdes for før de blir overskrevet. Må være større enn \"Begrens etter\". Funker kun om segmentsletting er påskrudd.", - "LabelLevel": "Nivå" + "LabelLevel": "Nivå", + "GoHome": "Hjem", + "UnknownError": "En ukjent feil har oppstått.", + "GridView": "Rutenett", + "ListView": "Listevisning", + "AllowAv1Encoding": "Tillat enkoding i AV1-format", + "AiTranslated": "AI-oversatt", + "MachineTranslated": "Maskinoversatt", + "HeaderGuestCast": "Gjestestjerner", + "HearingImpairedShort": "HI/SDH", + "LogoScreensaver": "Logoskjermsparer", + "LabelIsHearingImpaired": "For hørselshemmede (SDH)", + "LabelBackdropScreensaverInterval": "Skjermsparingsbakgrunn-intervall", + "LabelBackdropScreensaverIntervalHelp": "Tid i milisekunder mellom forskjellige bakgrunner ved bruk av skjermsparingsbakgrunner.", + "BackdropScreensaver": "Skjermsparingsbakgrunn", + "ForeignPartsOnly": "Kun tvungne/fremmede deler" } diff --git a/src/strings/nl.json b/src/strings/nl.json index c5e3496310..1a2f32fe42 100644 --- a/src/strings/nl.json +++ b/src/strings/nl.json @@ -1380,7 +1380,7 @@ "QuickConnectDescription": "Om je aan te melden met Quick Connect selecteer je de 'Quick Connect'-knop op het apparaat waarop je je wilt aanmelden en en voer je de weergegeven code hieronder in.", "QuickConnectDeactivated": "Quick Connect is gedeactiveerd voordat het aanmeldverzoek goedgekeurd kon worden", "QuickConnectAuthorizeFail": "Onbekende Quick Connect-code", - "QuickConnectAuthorizeSuccess": "Verzoek goedgekeurd", + "QuickConnectAuthorizeSuccess": "Je hebt je apparaat succesvol goedgekeurd!", "QuickConnectAuthorizeCode": "Vul code {0} in om aan te melden", "QuickConnectActivationSuccessful": "Succesvol geactiveerd", "LabelKnownProxies": "Bekende proxy's", @@ -1735,7 +1735,7 @@ "NotificationsMovedMessage": "De meldingsfunctionaliteit is verplaatst naar de Webhook-plug-in.", "EnableAudioNormalizationHelp": "Geluidsnormalisatie past een constante versterking toe om het gemiddelde op een gewenst niveau (-18dB) te houden.", "LabelEnableLUFSScan": "LUFS-scan inschakelen", - "LabelEnableLUFSScanHelp": "LUFS-scan voor muziek inschakelen. Dit duurt langer en is systeemintensief.", + "LabelEnableLUFSScanHelp": "Cliënten kunnen audio normaliseren zodat verschillende nummers even luid zijn. Dit is systeemintensief en bibliotheekscans zullen langer duren.", "PasswordRequiredForAdmin": "Voor beheerdersaccounts is een wachtwoord vereist.", "LabelSyncPlayNoGroups": "Geen groepen beschikbaar", "HeaderConfirmRepositoryInstallation": "Installatie plug-in-repository bevestigen", @@ -1770,5 +1770,10 @@ "ListView": "Lijstweergave", "AiTranslated": "Vertaald door AI", "HeaderGuestCast": "Gastrollen", - "HearingImpairedShort": "Voor slechthorenden" + "HearingImpairedShort": "ODS", + "GoHome": "Startpagina", + "UnknownError": "Er is een onbekende fout opgetreden.", + "BackdropScreensaver": "Schermbeveiliging met achtergronden", + "LogoScreensaver": "Schermbeveiliging met logo", + "LabelIsHearingImpaired": "Voor slechthorenden (ODS)" } diff --git a/src/strings/pl.json b/src/strings/pl.json index ef951021cd..aaa0b1ab57 100644 --- a/src/strings/pl.json +++ b/src/strings/pl.json @@ -1105,7 +1105,7 @@ "ThemeSongs": "Motywy muzyczne", "ThemeVideos": "Motywy wideo", "TheseSettingsAffectSubtitlesOnThisDevice": "Te ustawienia dotyczą napisów na tym urządzeniu", - "ThisWizardWillGuideYou": "Niniejszy kreator pomoże Ci przejść przez proces instalacji. Najpierw, wybierz preferowany przez siebie język.", + "ThisWizardWillGuideYou": "Niniejszy kreator pomoże Ci przejść przez proces instalacji. Najpierw wybierz preferowany przez siebie język.", "Thumb": "Miniatura", "Thursday": "Czwartek", "TitleHardwareAcceleration": "Akceleracja sprzętowa", @@ -1461,7 +1461,7 @@ "SyncPlayGroupDefaultTitle": "Grupa {0}", "QuickConnectNotAvailable": "Poproś administratora serwera, aby włączył szybkie łączenie", "QuickConnectNotActive": "Szybkie łączenie nie jest aktywne na tym serwerze", - "QuickConnectAuthorizeSuccess": "Żądanie autoryzowane", + "QuickConnectAuthorizeSuccess": "Pomyślnie uwierzytelniono urządzenie!", "QuickConnectAuthorizeCode": "Wpisz kod {0}, aby się zalogować", "PluginFromRepo": "{0} z repozytorium {1}", "OptionMaxActiveSessionsHelp": "Wartość \"0\" wyłączy tę funkcję.", @@ -1516,7 +1516,7 @@ "MessageSent": "Wiadomość została wysłana.", "TextSent": "Tekst został wysłany.", "LabelSlowResponseTime": "Czas w milisekundach po którym odpowiedź uznana będzie za powolną", - "LabelSlowResponseEnabled": "Zaloguj ostrzeżenie gdy serwer wolno odpowiada", + "LabelSlowResponseEnabled": "Zarejestruj ostrzeżenie, gdy serwer wolno odpowiada", "UseEpisodeImagesInNextUpHelp": "Sekcje 'Do obejrzenia' i 'Kontynuuj odtwarzanie' pokażą grafikę epizodu jako podgląd zamiast głównej miniatury serii.", "UseEpisodeImagesInNextUp": "Użyj grafik epizodów w sekcjach 'Do obejrzenia' i 'Kontynuuj odtwarzanie'", "AudioBitDepthNotSupported": "Głębia bitowa dźwięku nie jest obsługiwana", @@ -1733,7 +1733,7 @@ "EnableAudioNormalizationHelp": "Normalizacja dźwięku doda stałe wzmocnienie, aby utrzymać średnią na pożądanym poziomie (-18dB).", "EnableAudioNormalization": "Normalizacja dźwięku", "LabelEnableLUFSScan": "Włącz skanowanie głośności dźwięku", - "LabelEnableLUFSScanHelp": "Włącz skanowanie głośności dźwięku dla muzyki (To zajmie więcej czasu i więcej zasobów).", + "LabelEnableLUFSScanHelp": "Klienty mogą normalizować odtwarzanie dźwięku, aby uzyskać jednakową głośność na wszystkich ścieżkach. Wydłuży to skanowanie biblioteki i zajmie więcej zasobów.", "Notifications": "Powiadomienia", "PasswordRequiredForAdmin": "Hasło jest wymagane do kont administratorów.", "LabelSyncPlayNoGroups": "Brak dostępnych grup", @@ -1771,5 +1771,10 @@ "AiTranslated": "Przetłumaczono przez SI", "MachineTranslated": "Przetłumaczono maszynowo", "ForeignPartsOnly": "Tylko części wymuszone/obce", - "HearingImpairedShort": "HI/SDH" + "HearingImpairedShort": "HI/SDH", + "GoHome": "Wróć na start", + "UnknownError": "Wystąpił nieznany błąd.", + "BackdropScreensaver": "Wygaszacz ekranu z fototapetami", + "LogoScreensaver": "Wygaszacz ekranu z logo", + "LabelIsHearingImpaired": "Dla osób niedosłyszących" } diff --git a/src/strings/pt-br.json b/src/strings/pt-br.json index 5239d9d3c1..71068d9ba6 100644 --- a/src/strings/pt-br.json +++ b/src/strings/pt-br.json @@ -1751,5 +1751,25 @@ "Unknown": "Desconhecido", "HeaderConfirmRepositoryInstallation": "Confirme a instalação do repositório de plug-ins", "LabelDeveloper": "Desenvolvedor", - "PleaseConfirmRepositoryInstallation": "Por favor, clique em OK para confirmar que você leu o acima e deseja prosseguir com a instalação do repositório de plug-ins." + "PleaseConfirmRepositoryInstallation": "Por favor, clique em OK para confirmar que você leu o acima e deseja prosseguir com a instalação do repositório de plug-ins.", + "LabelIsHearingImpaired": "Para deficientes auditivos (SDH)", + "BackdropScreensaver": "Imagem de fundo do protetor de tela", + "LogoScreensaver": "Logo da proteção de tela", + "AllowAv1Encoding": "Permitir codificação em formato AV1", + "HeaderGuestCast": "Estrelas Convidadas", + "HeaderEpisodesStatus": "Situação dos Episódios", + "GoHome": "Tela Inicial", + "UnknownError": "Um erro desconhecido aconteceu.", + "LabelBackdropScreensaverInterval": "Intervalo da imagem de fundo da proteção de tela", + "LabelBackdropScreensaverIntervalHelp": "Tempo em segundos entre as diferentes imagens de fundo do protetor de tela.", + "GridView": "Visualização em grelha", + "ListView": "Visualização em Lista", + "AiTranslated": "Traduzido por IA", + "MachineTranslated": "Traduzido por Máquina", + "AllowSegmentDeletion": "Remover segmentos", + "AllowSegmentDeletionHelp": "Remover segmentos antigos após serem enviados ao cliente. Isso previne armazenar o arquivo transcodificado em disco. Funciona apenas com limitação habilitada. Desligue esta opção se você experenciar problemas com a reprodução.", + "LabelThrottleDelaySeconds": "Limitar após", + "LabelThrottleDelaySecondsHelp": "Tempo em segundos em que o transcodificador será limitado. É necessário que seja grande o suficiente para que o cliente mantenha um buffer saudável. Funciona apenas se o limitador estiver habilitado.", + "LabelSegmentKeepSeconds": "Tempo para armazenar seguimentos", + "LabelSegmentKeepSecondsHelp": "Tempo em segundos para que os seguimentos sejam armazenados antes de serem sobrescritos. É necessário que seja maior que \"Limitar após\". Funciona apenas se a remoção de segmentos estiver habilitada." } diff --git a/src/strings/pt.json b/src/strings/pt.json index 4356e6ad8f..8669e7502e 100644 --- a/src/strings/pt.json +++ b/src/strings/pt.json @@ -1421,5 +1421,11 @@ "LabelParallelImageEncodingLimit": "Limite de codificação de imagens em paralelo", "LabelEnableLUFSScan": "Habilitar busca LUFS", "LabelEnableLUFSScanHelp": "Habilitar busca LUFS para música (Isto necessitará de mais tempo e recursos).", - "LabelParallelImageEncodingLimitHelp": "Quantidade máxima de imagens codificadas que são permitidas rodar em paralelo. Ajustar este valor para 0 ira selecionar um limite baseado nas especificaçõrs do seu sistema." + "LabelParallelImageEncodingLimitHelp": "Quantidade máxima de imagens codificadas que são permitidas rodar em paralelo. Ajustar este valor para 0 ira selecionar um limite baseado nas especificaçõrs do seu sistema.", + "GoHome": "Ir ao início", + "GridView": "Visão em grade", + "HeaderConfirmRepositoryInstallation": "Confirme a instalação do repositório de plugin", + "AllowSegmentDeletion": "Deletar segmentos", + "AllowSegmentDeletionHelp": "Exclua segmentos antigos após terem sido enviados ao cliente. Isso evita ter que armazenar todo o arquivo transcodificado no disco. Funcionará apenas com throttling habilitado. Desligue esta opção se tiver problemas de reprodução.", + "LabelSegmentKeepSeconds": "Tempo em segundos para manter os segmentos" } diff --git a/src/strings/sk.json b/src/strings/sk.json index e7bd6c2fed..a4fea164af 100644 --- a/src/strings/sk.json +++ b/src/strings/sk.json @@ -1736,5 +1736,6 @@ "UserMenu": "Užívateľská ponuka", "LabelEnableLUFSScan": "Povoliť skenovanie LUFS", "LabelEnableLUFSScanHelp": "Povoliť skenovanie LUFS pre hudbu (Predlžuje skenovanie a je náročnejšie na výkon).", - "MenuOpen": "Otvoriť ponuku" + "MenuOpen": "Otvoriť ponuku", + "AllowSegmentDeletion": "Zmazať oddiel" } diff --git a/src/strings/sv.json b/src/strings/sv.json index a8e91a2f82..39b1c90a0a 100644 --- a/src/strings/sv.json +++ b/src/strings/sv.json @@ -1371,7 +1371,7 @@ "EnableQuickConnect": "Aktivera snabbanslutning på denna server", "QuickConnectDeactivated": "Snabbanslutning inaktiverades innan inloggningsförsöket kunde godkännas", "QuickConnectAuthorizeFail": "Okänd snabbanslutningskod", - "QuickConnectAuthorizeSuccess": "Anrop auktoriserat", + "QuickConnectAuthorizeSuccess": "Du har autentiserat din enhet!", "QuickConnectAuthorizeCode": "Ange kod {0} för att logga in", "QuickConnectActivationSuccessful": "Aktivering lyckades", "QuickConnect": "Snabbanslutning", @@ -1760,5 +1760,20 @@ "LabelDeveloper": "Utväcklare", "Notifications": "Notifieringar", "UserMenu": "Användarmeny", - "LabelTonemappingMode": "Tonmappningsläge" + "LabelTonemappingMode": "Tonmappningsläge", + "AllowAv1Encoding": "Tillåt kodning i AV1-format", + "GoHome": "Hem", + "UnknownError": "Ett okänt fel har uppstått.", + "HeaderGuestCast": "Gästroller", + "LabelBackdropScreensaverIntervalHelp": "Tiden i sekunder mellan olika bakgrunder när bakgrunds-skärmsläckaren används.", + "AiTranslated": "AI-översatt", + "MachineTranslated": "Maskinöversatt", + "HearingImpairedShort": "HI/SDH", + "BackdropScreensaver": "Bakgrunds skärmsläckare", + "LogoScreensaver": "Logotyp skärmsläckare", + "LabelIsHearingImpaired": "För hörselskadade (SDH)", + "LabelBackdropScreensaverInterval": "Bakgrunds skärmsläckare intervall", + "GridView": "Rutnätsvy", + "ListView": "Listvy", + "ForeignPartsOnly": "Forcerad/Främmande delar enbart" } diff --git a/src/strings/tr.json b/src/strings/tr.json index 8153695db1..8769e19dbd 100644 --- a/src/strings/tr.json +++ b/src/strings/tr.json @@ -18,8 +18,8 @@ "ButtonRemove": "Sil", "ButtonSelectDirectory": "Dosyayı Seç", "ButtonSend": "Gönder", - "ButtonSignIn": "Giriş Yap", - "ButtonSignOut": "Çıkış Yap", + "ButtonSignIn": "Oturum Aç", + "ButtonSignOut": "Oturumu Kapat", "ButtonStop": "Durdur", "ChannelAccessHelp": "Bu kullanıcıyla paylaşmak üzere kanalları seç. Yöneticiler bütün kanalları metada yöneticisi ile düzenleyebilecekler.", "Continuing": "Devam ediyor", @@ -49,7 +49,7 @@ "HeaderLibraryFolders": "Media Klasörleri", "HeaderMediaFolders": "Medya Klasörleri", "HeaderPlayAll": "Hepsini Oynat", - "HeaderPleaseSignIn": "Lütfen Giriş Yapın", + "HeaderPleaseSignIn": "Lütfen oturum açın", "HeaderPreferredMetadataLanguage": "Tercih Edilen Metaveri Dili", "HeaderRecentlyPlayed": "En Son Oynatılanlar", "HeaderRemoteControl": "Uzaktan Kontrol", @@ -908,7 +908,7 @@ "QuickConnectInvalidCode": "Geçersiz Hızlı Bağlantı kodu", "QuickConnectDescription": "Hızlı Bağlantı ile oturum açmak için, oturum açtığınız cihazda Hızlı Bağlan düğmesini seçin ve aşağıda görüntülenen kodu girin.", "QuickConnectDeactivated": "Giriş isteği onaylanamadan Hızlı Bağlantı devre dışı bırakıldı", - "QuickConnectAuthorizeSuccess": "Yetkili istek", + "QuickConnectAuthorizeSuccess": "Cihazınızın kimliğini başarıyla doğruladınız!", "QuickConnectAuthorizeFail": "Bilinmeyen Hızlı Bağlantı kodu", "QuickConnectAuthorizeCode": "Giriş yapmak için {0} kodunu girin", "QuickConnectActivationSuccessful": "Başarıyla etkinleştirildi", @@ -1297,7 +1297,7 @@ "LabelSyncPlaySettingsSkipToSyncHelp": "Tahmini konumu aramayı içeren senkronizasyon düzeltme yöntemi. Senkronizasyon Düzeltme etkinleştirilmelidir.", "LabelSyncPlaySettingsSkipToSync": "SkipToSync'i Etkinleştir", "LabelSyncPlaySettingsSpeedToSyncHelp": "Oynatmayı hızlandırmayı içeren senkronizasyon düzeltme yöntemi. Senkronizasyon Düzeltme etkinleştirilmelidir.", - "ButtonExitApp": "Çıkış uygulaması", + "ButtonExitApp": "Uygulamadan Çık", "ShowAdvancedSettings": "Gelişmiş ayarları göster", "MediaInfoTitle": "Başlık", "Track": "Parça", @@ -1726,7 +1726,7 @@ "MenuOpen": "Açık Menü", "LabelEnableAudioVbrHelp": "Değişken bit hızı, ortalama bit hızı oranına göre daha iyi kalite sunar, ancak bazı nadir durumlarda arabelleğe alma ve uyumluluk sorunlarına neden olabilir.", "LabelEnableLUFSScan": "LUFS taramasını etkinleştir", - "LabelEnableLUFSScanHelp": "Müzik için LUFS taramasını etkinleştirin (Bu daha uzun sürecek ve daha fazla kaynak gerektirecektir).", + "LabelEnableLUFSScanHelp": "İstemciler, parçalar arasında eşit ses yüksekliği elde etmek için ses oynatmayı normalleştirebilir. Bu, kütüphane taramalarını daha uzun hale getirecek ve daha fazla kaynak kullanacaktır.", "LabelDummyChapterDuration": "Aralık", "LabelChapterImageResolutionHelp": "Çıkarılan bölüm görüntülerinin çözünürlüğü. Bunu değiştirmenin mevcut sahte bölümler üzerinde hiçbir etkisi olmayacaktır.", "LabelParallelImageEncodingLimit": "Paralel görüntü kodlama sınırı", @@ -1755,5 +1755,18 @@ "LogLevel.None": "Hiçbiri", "LabelThrottleDelaySecondsHelp": "Kodek dönüştürmenin kısılacağı saniye cinsinden süre. İstemcinin sağlıklı bir arabellek tutması için yeterince büyük olmalıdır. Yalnızca kısıtlama etkinleştirildiğinde çalışır.", "LabelBackdropScreensaverInterval": "Arka Plan Ekran Koruyucu Arlığı", - "LabelBackdropScreensaverIntervalHelp": "Arka plan ekran koruyucusu kullanımdayken farklı arka planlar arasında geçen saniye cinsinden süre." + "LabelBackdropScreensaverIntervalHelp": "Arka plan ekran koruyucusu kullanımdayken farklı arka planlar arasında geçen saniye cinsinden süre.", + "AllowAv1Encoding": "AV1 biçiminde kodlamaya izin ver", + "BackdropScreensaver": "Arka Plan Ekran Koruyucu", + "LogoScreensaver": "Logo Ekran Koruyucu", + "LabelIsHearingImpaired": "İşitme engelliler için (SDH)", + "HeaderGuestCast": "Konuk Oyuncular", + "GoHome": "Ana Sayfaya Git", + "UnknownError": "Bilinmeyen bir hata oluştu.", + "GridView": "Izgara Görünümü", + "ListView": "Liste Görünümü", + "AiTranslated": "AI Çevirisi", + "MachineTranslated": "Makine Çevirisi", + "ForeignPartsOnly": "Gömülü/Yalnız yabancı parçalar", + "HearingImpairedShort": "HI/SDH" } diff --git a/src/strings/uk.json b/src/strings/uk.json index f2cbf649b3..583d7f09b6 100644 --- a/src/strings/uk.json +++ b/src/strings/uk.json @@ -1229,7 +1229,7 @@ "QuickConnectInvalidCode": "Недійсний код швидкого підключення", "QuickConnectDescription": "Щоб увійти за допомогою швидкого підключення, виберіть кнопку «Швидке підключення» на пристрої, з якого ви входите, і введіть код, що відображається нижче.", "QuickConnectDeactivated": "Швидке підключення було деактивовано до того, як запит на вхід було схвалено", - "QuickConnectAuthorizeSuccess": "Запит авторизований", + "QuickConnectAuthorizeSuccess": "Ви успішно автентифікували свій пристрій!", "QuickConnectAuthorizeFail": "Невідомий код швидкого підключення", "QuickConnectAuthorizeCode": "Введіть код {0} для входу", "QuickConnectActivationSuccessful": "Успішно активовано", @@ -1728,7 +1728,7 @@ "Studio": "Студія", "AllowCollectionManagement": "Дозволити цьому користувачеві керувати колекціями", "EnableAudioNormalizationHelp": "Нормалізація звуку додасть постійний коефіцієнт підсилення, щоб утримати середній рівень на потрібному рівні (-18 дБ).", - "LabelEnableLUFSScanHelp": "Увімкніть сканування LUFS для пошуку музики (це займе більше часу і ресурсів).", + "LabelEnableLUFSScanHelp": "Клієнти можуть нормалізувати відтворення звуку, щоб отримати однакову гучність на всіх доріжках. Це зробить сканування бібліотеки довшим і забиратиме більше ресурсів.", "EnableAudioNormalization": "Нормалізація звуку", "LabelEnableLUFSScan": "Увімкнути сканування LUFS", "GetThePlugin": "Отримати плагін", @@ -1768,5 +1768,10 @@ "HearingImpairedShort": "HI/SDH", "GridView": "У вигляді сітки", "ListView": "У вигляді списку", - "AiTranslated": "Перекладено за допомогою ШІ" + "AiTranslated": "Перекладено за допомогою ШІ", + "UnknownError": "Виникла невідома помилка.", + "GoHome": "На головну", + "BackdropScreensaver": "Фонова заставка", + "LogoScreensaver": "Заставка з логотипом", + "LabelIsHearingImpaired": "Для людей з вадами слуху (SDH)" } diff --git a/src/strings/vi.json b/src/strings/vi.json index 54afe66d4b..a53d197c41 100644 --- a/src/strings/vi.json +++ b/src/strings/vi.json @@ -814,7 +814,7 @@ "LabelSeasonNumber": "Số phần", "EnableFasterAnimationsHelp": "Sử dụng hoạt ảnh và chuyển tiếp nhanh hơn.", "EnableFasterAnimations": "Hoạt ảnh nhanh hơn", - "LabelScreensaver": "Bảo vệ màn hình", + "LabelScreensaver": "Màn hình chờ", "LabelRuntimeMinutes": "Thời lượng", "LabelRequireHttpsHelp": "Nếu chọn, máy chủ sẽ tự động chuyển hướng tất cả các yêu cầu qua HTTP sang HTTPS. Điều này không ảnh hưởng nếu máy chủ không nghe trên HTTPS.", "LabelRequireHttps": "Yêu cầu HTTPS", @@ -1221,7 +1221,7 @@ "QuickConnectDescription": "Để đăng nhập với Kết Nối Nhanh, hãy chọn nút 'Kết Nối Nhanh' trên thiết bị bạn đang đăng nhập và nhập mã hiển thị bên dưới.", "QuickConnectDeactivated": "Kết Nối Nhanh đã bị vô hiệu hóa trước khi yêu cầu đăng nhập được chấp nhận", "QuickConnectAuthorizeFail": "Mã Kết Nối Nhanh không xác định", - "QuickConnectAuthorizeSuccess": "Yêu cầu đã được cho phép", + "QuickConnectAuthorizeSuccess": "Bạn đã xác thực thành công thiết bị của mình!", "QuickConnectAuthorizeCode": "Nhập mã {0} để đăng nhập", "QuickConnectActivationSuccessful": "Kích hoạt thành công", "QuickConnect": "Kết Nối Nhanh", @@ -1724,7 +1724,7 @@ "Notifications": "Thông báo", "NotificationsMovedMessage": "Chức năng thông báo đã chuyển sang plugin Webhook.", "LabelEnableLUFSScan": "Bật tính năng quét LUFS", - "LabelEnableLUFSScanHelp": "Bật tính năng quét LUFS để tìm nhạc (Việc này sẽ mất nhiều thời gian hơn và tốn nhiều tài nguyên hơn).", + "LabelEnableLUFSScanHelp": "Khách hàng có thể bình thường hóa việc phát lại âm thanh để có được âm lượng như nhau trên các bản nhạc. Điều này sẽ khiến việc quét thư viện lâu hơn và tốn nhiều tài nguyên hơn.", "PasswordRequiredForAdmin": "Cần có mật khẩu cho tài khoản quản trị viên.", "LabelSyncPlayNoGroups": "Không có nhóm nào", "LabelDate": "Ngày", @@ -1747,6 +1747,15 @@ "AllowSegmentDeletion": "Xóa phân đoạn", "LabelSegmentKeepSeconds": "Thời gian giữ phân đoạn", "HeaderEpisodesStatus": "Trạng Thái Tập", - "LabelBackdropScreensaverInterval": "Thời Gian Phông Nền Màn Hình Bảo Vệ", - "LabelBackdropScreensaverIntervalHelp": "Thời gian tính bằng giây giữa các phông nền khác nhau khi sử dụng trình bảo vệ màn hình phông nền." + "LabelBackdropScreensaverInterval": "Thời Gian Phông Nền Màn Hình Chờ", + "LabelBackdropScreensaverIntervalHelp": "Thời gian tính bằng giây giữa các phông nền khác nhau khi dùng phông nền màn hình chờ.", + "LogoScreensaver": "Biểu Tượng Màn Hình Chờ", + "LabelIsHearingImpaired": "Dành cho người khiếm thính (SDH)", + "AllowAv1Encoding": "Cho phép mã hóa ở định dạng AV1", + "BackdropScreensaver": "Phông Nền Màn Hình Chờ", + "UnknownError": "Đã xảy ra lỗi không xác định.", + "HeaderGuestCast": "Ngôi Sao Khách Mời", + "GoHome": "Về Trang Chủ", + "AiTranslated": "Được AI Dịch", + "MachineTranslated": "Được Máy Dịch" } diff --git a/src/strings/zh-cn.json b/src/strings/zh-cn.json index ad9b426be0..8a0f36db6c 100644 --- a/src/strings/zh-cn.json +++ b/src/strings/zh-cn.json @@ -1188,7 +1188,7 @@ "PlayNextEpisodeAutomatically": "自动播放下一集", "Premieres": "首映", "Raised": "提高", - "Recordings": "录音", + "Recordings": "录制", "RefreshDialogHelp": "元数据根据设置和仪表盘中启用的网络服务进行刷新。", "RepeatEpisodes": "重复剧集", "Schedule": "日程", @@ -1415,7 +1415,7 @@ "QuickConnectDescription": "要使用快速连接登录,请点击要登录的设备上的快速连接按钮,然后在下方输入显示的验证码。", "QuickConnectDeactivated": "在批准登录请求之前,快速连接已被禁用", "QuickConnectAuthorizeFail": "未知的快速连接验证码", - "QuickConnectAuthorizeSuccess": "请求已被授权成功", + "QuickConnectAuthorizeSuccess": "您的设备验证成功!", "QuickConnectAuthorizeCode": "输入验证码 {0} 以登陆", "QuickConnectActivationSuccessful": "启用成功", "QuickConnect": "快速连接", @@ -1733,7 +1733,7 @@ "EnableAudioNormalizationHelp": "音频标准化将添加一个恒定的增益,以保持平均音量在所需的级别(-18dB)。", "EnableAudioNormalization": "音频标准化", "LabelEnableLUFSScan": "启用 LUFS 扫描", - "LabelEnableLUFSScanHelp": "启用音乐的LUFS扫描(这将需要更长时间和更多资源)。", + "LabelEnableLUFSScanHelp": "客户端可以将音频播放进行归一化,以实现曲目之间的音量均衡。这将导致库扫描时间更长,并消耗更多的资源。", "GetThePlugin": "获取插件", "Notifications": "通知", "NotificationsMovedMessage": "通知功能已经转移到Webhook插件中。", @@ -1771,5 +1771,7 @@ "AiTranslated": "AI翻译", "MachineTranslated": "机器翻译", "ForeignPartsOnly": "仅限强制开启/外语部分", - "HearingImpairedShort": "听障/聋哑人士字幕" + "HearingImpairedShort": "听障/聋哑人士字幕", + "UnknownError": "发生未知错误。", + "GoHome": "回家" } diff --git a/src/strings/zh-hk.json b/src/strings/zh-hk.json index b9416a426d..81784bf2f7 100644 --- a/src/strings/zh-hk.json +++ b/src/strings/zh-hk.json @@ -1155,5 +1155,8 @@ "ValueMinutes": "{0}分", "MinutesBefore": "分鐘前", "MessagePleaseWait": "請稍等。", - "AllowCollectionManagement": "允許用戶管理合輯" + "AllowCollectionManagement": "允許用戶管理合輯", + "AllowAv1Encoding": "允許AV1格式轉碼", + "AllowSegmentDeletion": "移除選段", + "AiTranslated": "AI 翻譯" } diff --git a/src/strings/zh-tw.json b/src/strings/zh-tw.json index dd498b898f..b4825af3bd 100644 --- a/src/strings/zh-tw.json +++ b/src/strings/zh-tw.json @@ -336,7 +336,7 @@ "DoNotRecord": "不錄製", "Down": "下", "DownloadsValue": "{0} 個下載", - "DrmChannelsNotImported": "受 DMR 保護的頻道將不會被導入。", + "DrmChannelsNotImported": "受 DRM 保護的頻道將不會被導入。", "DropShadow": "陰影", "EasyPasswordHelp": "你的簡易 PIN 碼將會用於在支援的 Jellyfin 應用上進行離線存取,同時也可被用於區域網路的登入。", "EditMetadata": "編輯媒體資訊", @@ -1762,5 +1762,11 @@ "HeaderConfirmRepositoryInstallation": "確認外掛來源庫", "MessageRepositoryInstallDisclaimer": "警告:安裝第三方外掛程式庫有風險。其中可能包不穩定或含惡意的程式,並且可能隨時變化。請只安裝你信任的作者提供的外掛程式庫。", "LabelEnableLUFSScanHelp": "針對音樂啟用 LUFS 掃描(需要更多時間和資源)。", - "AllowCollectionManagement": "允許用戶管理合輯" + "AllowCollectionManagement": "允許用戶管理合輯", + "GridView": "網格檢視", + "AllowAv1Encoding": "允許以 AV1 格式編碼", + "ListView": "清單檢視", + "AiTranslated": "AI翻譯", + "MachineTranslated": "機器翻譯", + "ForeignPartsOnly": "僅限強制或外語部分" } diff --git a/src/strings/zu.json b/src/strings/zu.json index 23e286b8fc..f388785dc5 100644 --- a/src/strings/zu.json +++ b/src/strings/zu.json @@ -113,5 +113,7 @@ "Aired": "Kusakazwa", "Add": "Yengeza", "Actor": "Umlingisi", - "Absolute": "Impela" + "Absolute": "Impela", + "HeaderAlbumArtists": "Abasethi wenkulumo", + "HeaderContinueWatching": "Buyela Ukubona" } diff --git a/src/apps/experimental/theme.ts b/src/themes/theme.ts similarity index 96% rename from src/apps/experimental/theme.ts rename to src/themes/theme.ts index d3f84366be..e223e24c51 100644 --- a/src/apps/experimental/theme.ts +++ b/src/themes/theme.ts @@ -1,5 +1,6 @@ import { createTheme } from '@mui/material/styles'; +/** The default Jellyfin app theme for mui */ const theme = createTheme({ palette: { mode: 'dark', diff --git a/src/types/library.ts b/src/types/library.ts index 4bf275abe3..862ee3d6d3 100644 --- a/src/types/library.ts +++ b/src/types/library.ts @@ -62,6 +62,5 @@ export interface LibraryViewSettings { ShowTitle: boolean; ShowYear?: boolean; Filters?: Filters; - NameLessThan?: string | null; - NameStartsWith?: string | null; + Alphabet?: string | null; } diff --git a/src/utils/card.ts b/src/utils/card.ts new file mode 100644 index 0000000000..c3f047a79a --- /dev/null +++ b/src/utils/card.ts @@ -0,0 +1,20 @@ +enum CardShape { + Backdrop = 'backdrop', + BackdropOverflow = 'overflowBackdrop', + Portrait = 'portrait', + PortraitOverflow = 'overflowPortrait', + Square = 'square', + SquareOverflow = 'overflowSquare' +} + +export function getSquareShape(enableOverflow = true) { + return enableOverflow ? CardShape.SquareOverflow : CardShape.Square; +} + +export function getBackdropShape(enableOverflow = true) { + return enableOverflow ? CardShape.BackdropOverflow : CardShape.Backdrop; +} + +export function getPortraitShape(enableOverflow = true) { + return enableOverflow ? CardShape.PortraitOverflow : CardShape.Portrait; +} diff --git a/src/utils/file.ts b/src/utils/file.ts new file mode 100644 index 0000000000..b85178e2f2 --- /dev/null +++ b/src/utils/file.ts @@ -0,0 +1,15 @@ +/** + * Reads and returns the file encoded in base64 + */ +export function readFileAsBase64(file: File): Promise { + return new Promise(function (resolve, reject) { + const reader = new FileReader(); + reader.onload = (e) => { + // Split by a comma to remove the url: prefix + const data = (e.target?.result as string)?.split?.(',')[1]; + resolve(data); + }; + reader.onerror = reject; + reader.readAsDataURL(file); + }); +} diff --git a/src/utils/items.ts b/src/utils/items.ts new file mode 100644 index 0000000000..08936a3fde --- /dev/null +++ b/src/utils/items.ts @@ -0,0 +1,158 @@ +import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields'; +import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type'; +import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by'; +import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order'; +import * as userSettings from 'scripts/settings/userSettings'; +import { EpisodeFilter, FeatureFilters, LibraryViewSettings, ParentId, VideoBasicFilter, ViewMode } from '../types/library'; +import { LibraryTab } from 'types/libraryTab'; + +export const getVideoBasicFilter = (libraryViewSettings: LibraryViewSettings) => { + let isHd; + + if (libraryViewSettings.Filters?.VideoBasicFilter?.includes(VideoBasicFilter.IsHD)) { + isHd = true; + } + + if (libraryViewSettings.Filters?.VideoBasicFilter?.includes(VideoBasicFilter.IsSD)) { + isHd = false; + } + + return { + isHd, + is4K: libraryViewSettings.Filters?.VideoBasicFilter?.includes(VideoBasicFilter.Is4K) ? + true : + undefined, + is3D: libraryViewSettings.Filters?.VideoBasicFilter?.includes(VideoBasicFilter.Is3D) ? + true : + undefined + }; +}; + +export const getFeatureFilters = (libraryViewSettings: LibraryViewSettings) => { + return { + hasSubtitles: libraryViewSettings.Filters?.Features?.includes(FeatureFilters.HasSubtitles) ? + true : + undefined, + hasTrailer: libraryViewSettings.Filters?.Features?.includes(FeatureFilters.HasTrailer) ? + true : + undefined, + hasSpecialFeature: libraryViewSettings.Filters?.Features?.includes( + FeatureFilters.HasSpecialFeature + ) ? + true : + undefined, + hasThemeSong: libraryViewSettings.Filters?.Features?.includes(FeatureFilters.HasThemeSong) ? + true : + undefined, + hasThemeVideo: libraryViewSettings.Filters?.Features?.includes( + FeatureFilters.HasThemeVideo + ) ? + true : + undefined + }; +}; + +export const getEpisodeFilter = ( + viewType: LibraryTab, + libraryViewSettings: LibraryViewSettings +) => { + return { + parentIndexNumber: libraryViewSettings.Filters?.EpisodeFilter?.includes( + EpisodeFilter.ParentIndexNumber + ) ? + 0 : + undefined, + isMissing: + viewType === LibraryTab.Episodes ? + !!libraryViewSettings.Filters?.EpisodeFilter?.includes(EpisodeFilter.IsMissing) : + undefined, + isUnaired: libraryViewSettings.Filters?.EpisodeFilter?.includes(EpisodeFilter.IsUnaired) ? + true : + undefined + }; +}; + +const getItemFieldsEnum = ( + viewType: LibraryTab, + libraryViewSettings: LibraryViewSettings +) => { + const itemFields: ItemFields[] = []; + + if (viewType !== LibraryTab.Networks) { + itemFields.push(ItemFields.BasicSyncInfo, ItemFields.MediaSourceCount); + } + + if (libraryViewSettings.ImageType === ImageType.Primary) { + itemFields.push(ItemFields.PrimaryImageAspectRatio); + } + + if (viewType === LibraryTab.Networks) { + itemFields.push( + ItemFields.DateCreated, + ItemFields.PrimaryImageAspectRatio + ); + } + + return itemFields; +}; + +export const getFieldsQuery = ( + viewType: LibraryTab, + libraryViewSettings: LibraryViewSettings +) => { + return { + fields: getItemFieldsEnum(viewType, libraryViewSettings) + }; +}; + +export const getLimitQuery = () => { + return { + limit: userSettings.libraryPageSize(undefined) || undefined + }; +}; + +export const getAlphaPickerQuery = (libraryViewSettings: LibraryViewSettings) => { + const alphabetValue = libraryViewSettings.Alphabet !== null ? + libraryViewSettings.Alphabet : undefined; + + return { + nameLessThan: alphabetValue === '#' ? 'A' : undefined, + nameStartsWith: alphabetValue === '#' ? undefined : alphabetValue + }; +}; + +export const getFiltersQuery = ( + viewType: LibraryTab, + libraryViewSettings: LibraryViewSettings +) => { + return { + ...getFeatureFilters(libraryViewSettings), + ...getEpisodeFilter(viewType, libraryViewSettings), + ...getVideoBasicFilter(libraryViewSettings), + seriesStatus: libraryViewSettings?.Filters?.SeriesStatus, + videoTypes: libraryViewSettings?.Filters?.VideoTypes, + filters: libraryViewSettings?.Filters?.Status, + genres: libraryViewSettings?.Filters?.Genres, + officialRatings: libraryViewSettings?.Filters?.OfficialRatings, + tags: libraryViewSettings?.Filters?.Tags, + years: libraryViewSettings?.Filters?.Years, + studioIds: libraryViewSettings?.Filters?.StudioIds + }; +}; + +export const getSettingsKey = (viewType: LibraryTab, parentId: ParentId) => { + return `${viewType} - ${parentId}`; +}; + +export const getDefaultLibraryViewSettings = (): LibraryViewSettings => { + return { + ShowTitle: true, + ShowYear: false, + ViewMode: ViewMode.GridView, + ImageType: ImageType.Primary, + CardLayout: false, + SortBy: ItemSortBy.SortName, + SortOrder: SortOrder.Ascending, + StartIndex: 0 + }; +}; diff --git a/src/utils/jellyfin-apiclient/getItems.ts b/src/utils/jellyfin-apiclient/getItems.ts index 4bbe711f81..37d35f840e 100644 --- a/src/utils/jellyfin-apiclient/getItems.ts +++ b/src/utils/jellyfin-apiclient/getItems.ts @@ -67,7 +67,7 @@ function mergeResults(results: BaseItemDtoQueryResult[]) { export function getItems(apiClient: ApiClient, userId: string, options?: GetItemsRequest) { const ids = options?.Ids?.split(','); if (!options || !ids || ids.length <= ITEMS_PER_REQUEST_LIMIT) { - return apiClient.getItems(apiClient.getCurrentUserId(), options); + return apiClient.getItems(userId, options); } const results = getItemsSplit(apiClient, userId, options); diff --git a/src/utils/number.ts b/src/utils/number.ts index 16797d7d51..553280c1fe 100644 --- a/src/utils/number.ts +++ b/src/utils/number.ts @@ -32,3 +32,16 @@ export function toPercent(value: number | null | undefined, locale: string): str return `${Math.round(value * 100)}%`; } + +/** + * Gets decimal count of a Number. + * @param {number} value Number. + * @returns {number} Decimal count of a Number. + */ +export function decimalCount(value: number): number { + if (Number.isInteger(value)) return 0; + + const arr = value.toString().split('.'); + + return arr[1].length; +}