From 60102f28b645fb7b0d1e6c1683f3ba165f49ad16 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Thu, 28 Sep 2023 02:22:58 -0400 Subject: [PATCH 01/31] Refactor GH actions --- .github/workflows/{tsc.yml => build.yml} | 23 ++++++--- .../{codeql-analysis.yml => codeql.yml} | 23 +++++---- .github/workflows/{lint.yml => quality.yml} | 48 ++++++++++++++++++- .../workflows/{repo-stale.yaml => stale.yml} | 0 4 files changed, 76 insertions(+), 18 deletions(-) rename .github/workflows/{tsc.yml => build.yml} (56%) rename .github/workflows/{codeql-analysis.yml => codeql.yml} (71%) rename .github/workflows/{lint.yml => quality.yml} (68%) rename .github/workflows/{repo-stale.yaml => stale.yml} (100%) diff --git a/.github/workflows/tsc.yml b/.github/workflows/build.yml similarity index 56% rename from .github/workflows/tsc.yml rename to .github/workflows/build.yml index 35bde340f9..e6d71a2408 100644 --- a/.github/workflows/tsc.yml +++ b/.github/workflows/build.yml @@ -1,14 +1,19 @@ -name: TypeScript Build Check +name: Build + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true on: push: branches: [ master, release* ] pull_request: branches: [ master, release* ] + workflow_dispatch: jobs: - tsc: - name: Run TypeScript build check + run-build-prod: + name: Run production build runs-on: ubuntu-latest steps: @@ -25,8 +30,12 @@ jobs: - name: Install Node.js dependencies run: npm ci --no-audit - - name: Run tsc - run: npm run build:check + - name: Run a production build + run: npm run build:production - - name: Run test suite - run: npm run test + - name: Upload artifact + uses: actions/upload-artifact@v3.1.3 + with: + name: jellyfin-web__prod + path: | + dist diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql.yml similarity index 71% rename from .github/workflows/codeql-analysis.yml rename to .github/workflows/codeql.yml index 6c8ffdbbf1..3e08d9ce1f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql.yml @@ -1,31 +1,34 @@ -name: "CodeQL" +name: CodeQL + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true on: push: - branches: [ master ] + branches: [ master, release* ] pull_request: - branches: [ master ] + branches: [ master, release* ] schedule: - cron: '30 7 * * 6' jobs: - analyze: - name: Analyze + codeql: + name: CodeQL runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - language: [ 'javascript' ] steps: - name: Checkout repository uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Initialize CodeQL uses: github/codeql-action/init@46ed16ded91731b2df79a2893d3aea8e9f03b5c4 # v2.20.3 with: - languages: ${{ matrix.language }} + languages: javascript queries: +security-extended + - name: Autobuild uses: github/codeql-action/autobuild@46ed16ded91731b2df79a2893d3aea8e9f03b5c4 # v2.20.3 + - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@46ed16ded91731b2df79a2893d3aea8e9f03b5c4 # v2.20.3 diff --git a/.github/workflows/lint.yml b/.github/workflows/quality.yml similarity index 68% rename from .github/workflows/lint.yml rename to .github/workflows/quality.yml index b754665bca..b4840d90c4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/quality.yml @@ -1,4 +1,8 @@ -name: Lint +name: Quality checks + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true on: push: @@ -99,3 +103,45 @@ jobs: - name: Run stylelint run: npm run stylelint:scss + + run-tsc: + name: Run TypeScript build check + runs-on: ubuntu-latest + + steps: + - name: Check out Git repository + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + + - name: Setup node environment + uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 + with: + node-version: 20 + check-latest: true + cache: npm + + - name: Install Node.js dependencies + run: npm ci --no-audit + + - name: Run tsc + run: npm run build:check + + run-test: + name: Run tests + runs-on: ubuntu-latest + + steps: + - name: Check out Git repository + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + + - name: Setup node environment + uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 + with: + node-version: 20 + check-latest: true + cache: npm + + - name: Install Node.js dependencies + run: npm ci --no-audit + + - name: Run test suite + run: npm run test diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/stale.yml similarity index 100% rename from .github/workflows/repo-stale.yaml rename to .github/workflows/stale.yml From 203102c4b9f63ce8229e0f660442a29997c5262c Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Thu, 28 Sep 2023 18:57:00 -0400 Subject: [PATCH 02/31] Add CF pages publish actions --- .github/workflows/job-messages.yml | 65 +++++++++++++++++++++ .github/workflows/publish.yml | 94 ++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 .github/workflows/job-messages.yml create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/job-messages.yml b/.github/workflows/job-messages.yml new file mode 100644 index 0000000000..0e47838ba9 --- /dev/null +++ b/.github/workflows/job-messages.yml @@ -0,0 +1,65 @@ +name: Job messages + +on: + workflow_call: + inputs: + branch: + required: false + type: string + commit: + required: true + type: string + preview_url: + required: false + type: string + build_workflow_run_id: + required: false + type: number + commenting_workflow_run_id: + required: true + type: string + in_progress: + required: true + type: boolean + outputs: + msg: + description: The composed message + value: ${{ jobs.msg.outputs.msg }} + marker: + description: Hidden marker to detect PR comments composed by the bot + value: "CFPages-deployment" + +jobs: + msg: + name: Deployment status + runs-on: ubuntu-latest + outputs: + msg: ${{ env.msg }} + + steps: + - name: Compose message + if: ${{ always() }} + id: compose + env: + COMMIT: ${{ inputs.commit }} + PREVIEW_URL: ${{ inputs.preview_url != '' && (inputs.branch != 'master' && inputs.preview_url || format('https://jf-web.pages.dev ({0})', inputs.preview_url)) || 'Not available' }} + DEPLOY_STATUS: ${{ inputs.in_progress && '🔄 Deploying...' || (inputs.preview_url != '' && '✅ Deployed!' || '❌ Failure. Check workflow logs for details') }} + DEPLOYMENT_TYPE: ${{ inputs.branch != 'master' && '🔀 Preview' || '⚙️ Production' }} + BUILD_WORKFLOW_RUN: ${{ !inputs.in_progress && format('**[View build logs](https://github.com/{0}/actions/runs/{1})**', 'jellyfin/jellyfin-web', inputs.build_workflow_run_id) || '' }} + COMMENTING_WORKFLOW_RUN: ${{ format('**[View bot logs](https://github.com/{0}/actions/runs/{1})**', 'jellyfin/jellyfin-web', inputs.commenting_workflow_run_id) }} + # EOF is needed for multiline environment variables in a GitHub Actions context + run: | + echo "## Cloudflare Pages deployment" > $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| **Latest commit** | ${COMMIT::7} |" >> $GITHUB_STEP_SUMMARY + echo "|------------------------- |:----------------------------: |" >> $GITHUB_STEP_SUMMARY + echo "| **Status** | $DEPLOY_STATUS |" >> $GITHUB_STEP_SUMMARY + echo "| **Preview URL** | $PREVIEW_URL |" >> $GITHUB_STEP_SUMMARY + echo "| **Type** | $DEPLOYMENT_TYPE |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "$BUILD_WORKFLOW_RUN" >> $GITHUB_STEP_SUMMARY + echo "$COMMENTING_WORKFLOW_RUN" >> $GITHUB_STEP_SUMMARY + COMPOSED_MSG=$(cat $GITHUB_STEP_SUMMARY) + echo "msg<> $GITHUB_ENV + echo "$COMPOSED_MSG" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000000..2b7bbea712 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,94 @@ +on: + workflow_run: + workflows: + - Build + types: + - completed + +jobs: + publish: + permissions: + contents: read + deployments: write + + name: Deploy to Cloudflare Pages + runs-on: ubuntu-latest + outputs: + url: ${{ steps.cf.outputs.url }} + + steps: + - name: Download workflow artifact + uses: dawidd6/action-download-artifact@v2.27.0 + with: + run_id: ${{ github.event.workflow_run.id }} + name: jellyfin-web__prod + path: dist + + - name: Publish + id: cf + uses: cloudflare/pages-action@1 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + projectName: jf-web + directory: dist + gitHubToken: ${{ secrets.GITHUB_TOKEN }} + + pr-context: + name: PR context + if: ${{ always() && github.event.workflow_run.event == 'pull_request' }} + runs-on: ubuntu-latest + outputs: + commit: ${{ env.pr_sha }} + pr_number: ${{ env.pr_number }} + + steps: + - name: Get PR context + uses: dawidd6/action-download-artifact@v2.27.0 + id: pr_context + with: + run_id: ${{ github.event.workflow_run.id }} + name: PR_context + + - name: Set PR context environment variables + if: ${{ steps.pr_context.conclusion == 'success' }} + run: | + echo "pr_number=$(cat PR_number)" >> $GITHUB_ENV + echo "pr_sha=$(cat PR_sha)" >> $GITHUB_ENV + + compose-comment: + name: Compose comment + if: ${{ always() }} + uses: ./.github/workflows/job-messages.yml + needs: + - publish + - pr-context + + with: + branch: ${{ github.event.workflow_run.head_branch }} + commit: ${{ needs.pr-context.outputs.commit != '' && needs.pr-context.outputs.commit || github.event.workflow_run.head_sha }} + preview_url: ${{ needs.publish.outputs.url }} + build_workflow_run_id: ${{ github.event.workflow_run.id }} + commenting_workflow_run_id: ${{ github.run_id }} + in_progress: false + + comment-status: + name: Create comment status + if: | + always() && + github.event.workflow_run.event == 'pull_request' && + needs.pr-context.outputs.pr_number != '' + runs-on: ubuntu-latest + needs: + - compose-comment + - pr-context + + steps: + - name: Update job summary in PR comment + uses: thollander/actions-comment-pull-request@v2.4.2 + with: + GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }} + message: ${{ needs.compose-comment.outputs.msg }} + pr_number: ${{ needs.pr-context.outputs.pr_number }} + comment_tag: ${{ needs.compose-comment.outputs.marker }} + mode: recreate From c1b3a3f60d394c28bc5d51c5c269ff1b592b18f0 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Fri, 29 Sep 2023 00:34:49 -0400 Subject: [PATCH 03/31] Update codeql job name --- .github/workflows/codeql.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3e08d9ce1f..34d1fafbf1 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -14,7 +14,7 @@ on: jobs: codeql: - name: CodeQL + name: Run CodeQL runs-on: ubuntu-latest steps: From f6946f9898bebef934d844797150cf9c8085d31c Mon Sep 17 00:00:00 2001 From: arminius Date: Fri, 6 Oct 2023 03:55:11 +0200 Subject: [PATCH 04/31] Remove cache from docker images after installing packages --- deployment/Dockerfile.centos | 3 ++- deployment/Dockerfile.debian | 3 ++- deployment/Dockerfile.docker | 2 +- deployment/Dockerfile.fedora | 5 +++-- deployment/Dockerfile.portable | 3 ++- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/deployment/Dockerfile.centos b/deployment/Dockerfile.centos index 32ade8de43..39a8b1fbe3 100644 --- a/deployment/Dockerfile.centos +++ b/deployment/Dockerfile.centos @@ -14,7 +14,8 @@ RUN yum update -y \ && yum install -y epel-release \ && yum install -y rpmdevtools git autoconf automake glibc-devel gcc-c++ make \ && 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 + && yum install nodejs -y --setopt=nodesource-nodejs.module_hotfixes=1 \ + && yum clean all # 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 d42b812bf3..68c62cf792 100644 --- a/deployment/Dockerfile.debian +++ b/deployment/Dockerfile.debian @@ -17,7 +17,8 @@ RUN apt-get update \ && 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 + && apt-get install -y nodejs \ + && rm -rf /var/lib/apt/lists/* # 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 bd5a939962..46271bc9ae 100644 --- a/deployment/Dockerfile.docker +++ b/deployment/Dockerfile.docker @@ -3,7 +3,7 @@ FROM node:20-alpine ARG SOURCE_DIR=/src ARG ARTIFACT_DIR=/jellyfin-web -RUN apk add autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 +RUN apk --no-cache add autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 WORKDIR ${SOURCE_DIR} COPY . . diff --git a/deployment/Dockerfile.fedora b/deployment/Dockerfile.fedora index 8c77fae0cf..efcd602ce9 100644 --- a/deployment/Dockerfile.fedora +++ b/deployment/Dockerfile.fedora @@ -11,8 +11,9 @@ ENV IS_DOCKER=YES # Prepare Fedora environment RUN dnf update -y \ - && 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 + && dnf 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 \ + && dnf clean all # 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 e57052178b..4bdbe80a72 100644 --- a/deployment/Dockerfile.portable +++ b/deployment/Dockerfile.portable @@ -16,7 +16,8 @@ RUN apt-get update \ && 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 + && apt-get install -y nodejs \ + && rm -rf /var/lib/apt/lists/* # Link to build script RUN ln -sf ${SOURCE_DIR}/deployment/build.portable /build.sh From 50c3e04aacaf71a8291de8bb59e0e5a7709636f4 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Mon, 9 Oct 2023 10:17:54 -0400 Subject: [PATCH 05/31] Update CF Pages project name --- .github/workflows/job-messages.yml | 2 +- .github/workflows/publish.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/job-messages.yml b/.github/workflows/job-messages.yml index 0e47838ba9..7a58ecce76 100644 --- a/.github/workflows/job-messages.yml +++ b/.github/workflows/job-messages.yml @@ -42,7 +42,7 @@ jobs: id: compose env: COMMIT: ${{ inputs.commit }} - PREVIEW_URL: ${{ inputs.preview_url != '' && (inputs.branch != 'master' && inputs.preview_url || format('https://jf-web.pages.dev ({0})', inputs.preview_url)) || 'Not available' }} + PREVIEW_URL: ${{ inputs.preview_url != '' && (inputs.branch != 'master' && inputs.preview_url || format('https://jellyfin-web.pages.dev ({0})', inputs.preview_url)) || 'Not available' }} DEPLOY_STATUS: ${{ inputs.in_progress && '🔄 Deploying...' || (inputs.preview_url != '' && '✅ Deployed!' || '❌ Failure. Check workflow logs for details') }} DEPLOYMENT_TYPE: ${{ inputs.branch != 'master' && '🔀 Preview' || '⚙️ Production' }} BUILD_WORKFLOW_RUN: ${{ !inputs.in_progress && format('**[View build logs](https://github.com/{0}/actions/runs/{1})**', 'jellyfin/jellyfin-web', inputs.build_workflow_run_id) || '' }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2b7bbea712..1fcf65711e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -30,7 +30,7 @@ jobs: with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - projectName: jf-web + projectName: jellyfin-web directory: dist gitHubToken: ${{ secrets.GITHUB_TOKEN }} From 6c5393d2d397d6e42ef346e3d49e5aaf19ac176c Mon Sep 17 00:00:00 2001 From: Stas Ivanov Date: Mon, 9 Oct 2023 21:50:57 +0000 Subject: [PATCH 06/31] Translated using Weblate (Russian) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ru/ --- src/strings/ru.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/strings/ru.json b/src/strings/ru.json index 4b1cd2c2a2..d584214bd0 100644 --- a/src/strings/ru.json +++ b/src/strings/ru.json @@ -1,6 +1,6 @@ { "Absolute": "Абсолютный", - "AccessRestrictedTryAgainLater": "В настоящее время доступ запрещён. Повторите попытку позже.", + "AccessRestrictedTryAgainLater": "В настоящее время доступ ограничен. Повторите попытку позже.", "Actor": "Актёр", "Add": "Добавить", "AddToCollection": "Добавить в коллекцию", @@ -1763,5 +1763,8 @@ "LabelSegmentKeepSeconds": "Время сохранения сегментов", "LogLevel.Error": "Ошибка", "LabelBackdropScreensaverInterval": "Интервал между фонами у заставки", - "LabelBackdropScreensaverIntervalHelp": "Время в секундах между разными фонами, когда используется заставка." + "LabelBackdropScreensaverIntervalHelp": "Время в секундах между разными фонами, когда используется заставка.", + "BackdropScreensaver": "Заставка", + "GoHome": "Домой", + "GridView": "Сеткой" } From 1320f96c719c973f4b42ac7c78c51a4915d815e3 Mon Sep 17 00:00:00 2001 From: SaddFox Date: Mon, 9 Oct 2023 20:13:19 +0000 Subject: [PATCH 07/31] Translated using Weblate (Slovenian) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sl/ --- src/strings/sl-si.json | 74 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 65 insertions(+), 9 deletions(-) diff --git a/src/strings/sl-si.json b/src/strings/sl-si.json index d583cf90d6..34164f0f64 100644 --- a/src/strings/sl-si.json +++ b/src/strings/sl-si.json @@ -648,7 +648,7 @@ "MessageNoMovieSuggestionsAvailable": "Trenutno ni na voljo nobenih predlogov za filme. Začnite gledati in ocenjevati vaše filme, ter se nato vrnite sem in si oglejte predloge.", "LabelSelectFolderGroups": "Samodejno združi vsebine iz spodnjih map v poglede kot so 'Filmi', 'Glasba' in 'TV'", "TitlePlayback": "Predvajanje", - "MessagePasswordResetForUsers": "Gesla naslednjih uporabnikov so bila ponastavljena. Zdaj se lahko prijavijo z Enostavnimi PIN kodami, ki so bile uporabljene za ponastavitev.", + "MessagePasswordResetForUsers": "Gesla naslednjih uporabnikov so bila ponastavljena. Zdaj se lahko prijavijo s PIN kodami, ki so bile uporabljene za ponastavitev.", "OptionHideUserFromLoginHelp": "Koristno za zasebne ali skrite skrbniške račune. Uporabnik se bo moral prijaviti ročno z vpisom svojega uporabniškega imena in gesla.", "OnlyForcedSubtitlesHelp": "Naložijo se zgolj podnapisi, ki so označeni kot prisiljeni.", "OptionEnableExternalContentInSuggestionsHelp": "Dovoli, da so spletni napovedniki in TV kanali v živo vključeni med priporočenimi vsebinami.", @@ -810,7 +810,7 @@ "OptionAutomaticallyGroupSeries": "Samodejno združi serije, ki so razdeljene po več mapah", "OptionAllowUserToManageServer": "Dovoli temu uporabniku upravljanje strežnika", "MessageTheFollowingLocationWillBeRemovedFromLibrary": "Naslednja mesta predstavnosti bodo odstranjena iz vaše knjižnice", - "MessagePluginInstallDisclaimer": "Dodatki ustvarjeni s strani članov skupnosti so odličen način za izboljšanje vaše izkušnje z dodatnimi funkcijami in prednostmi. Preden namestite dodatke se zavedajte, da imajo lahko ti različne vplive na vaš strežnik, kot na primer počasnejše preiskovanje knjižnic, dodatna obdelava podatkov v ozadju in zmanjšana stabilnost sistema.", + "MessagePluginInstallDisclaimer": "POZOR: Nameščanje dodatkov tretjih oseb predstavlja tveganje. Vsebujejo lahko nestabilno ali zlonamerno kodo, ki se lahko kadarkoli spremeni. Namestite zgolj dodatke avtorjev ki jim zaupate. Zavedajte se morebitnih stranskih učinkov, kot na primer komunikacija z zunanjimi storitvami, podaljšan čas pregledovanja knjižnice in dodatno procesiranje v ozadju.", "MessagePleaseWait": "Prosimo, počakajte. To lahko traja nekaj minut.", "MessagePleaseEnsureInternetMetadata": "Prosimo poskrbite, da je prenašanje spletnih metapodatkov omogočeno.", "MessageNothingHere": "Tu ni ničesar.", @@ -1308,7 +1308,7 @@ "QuickConnectDescription": "Za vpis s Hitro povezavo izberi 'Hitra povezava' na napravi preko katere se vpisuješ in vnesi kodo.", "QuickConnectDeactivated": "Hitra povezava je bila onemogočena pred dokončanjem vpisa", "QuickConnectAuthorizeFail": "Neznana koda za Hitro povezavo", - "QuickConnectAuthorizeSuccess": "Avtorizirano", + "QuickConnectAuthorizeSuccess": "Uspešno ste overili svojo napravo!", "QuickConnectAuthorizeCode": "Vnesi kodo {0} za vpis", "QuickConnectActivationSuccessful": "Aktivirano", "QuickConnect": "Hitra povezava", @@ -1402,7 +1402,7 @@ "LabelTonemappingAlgorithm": "Izberite algoritem za preslikavo barv", "LabelOpenclDeviceHelp": "To je naprava OpenCL, ki bo uporabljena za preslikavo barv. Na levi strani pike je številka platforme, desno je številka naprave na tej platformi. Privzeta vrednost je 0.0. Zahtevana je datoteka FFmpeg, ki vsebuje metodo strojnega pospeševanja OpenCL.", "LabelColorPrimaries": "Barvni prostor", - "AllowTonemappingHelp": "Preslikava barv lahko preslika dinamični razpon videa HDR v SDR, pri tem pa ohranja podrobnosti in barve, kar je zelo pomembno za predstavitev izvorne scene. Trenutno deluje zgolj z HDR10 in HLG videi. Zahteva ustrezne OpenCL ali CUDA knjižnice.", + "AllowTonemappingHelp": "Preslikava barv lahko preslika dinamični razpon videa iz HDR v SDR, pri tem pa ohranja podrobnosti in barve, kar je zelo pomembno za predstavitev izvorne scene. Trenutno deluje zgolj z HDR10 in HLG videi. Zahteva ustrezne OpenCL ali CUDA knjižnice.", "MediaInfoVideoRange": "Barvni razpon", "LabelVideoRange": "Barvni razpon", "LabelSonyAggregationFlags": "Sonyjeve agregacijske oznake", @@ -1647,10 +1647,10 @@ "LabelUserMaxActiveSessions": "Največje število hkratnih uporabniških sej", "LabelUDPPortRangeHelp": "Omeji Jellyfin na ta razpon vrat za UDP komunikacije. (Privzeto 1024 - 645535).
Opomba: Nekatere funkcije zahtevajo fiksna vrata, ki so lahko izven tega razpona.", "LabelUDPPortRange": "Razpon komunikacij UDP", - "LabelVppTonemappingContrastHelp": "Uporabi povečanje kontrasta pri VPP preslikavi barv. Priporočena in privzeta vrednost je 0.", + "LabelVppTonemappingContrastHelp": "Uporabi povečanje kontrasta pri VPP preslikavi barv. Priporočeni in privzeti vrednosti sta 1.", "LabelVppTonemappingBrightness": "VPP preslikava barv povečanje svetlosti", "LabelVppTonemappingContrast": "VPP preslikava barv povečanje kontrasta", - "LabelVppTonemappingBrightnessHelp": "Uporabi povečanje svetlosti pri VPP preslikavi barv. Priporočena in privzeta vrednost je 0.", + "LabelVppTonemappingBrightnessHelp": "Uporabi povečanje svetlosti pri VPP preslikavi barv. Priporočeni in privzeti vrednosti sta 16 in 0.", "AllowVppTonemappingHelp": "Preslikava barv v celoti na podlagi gonilnikov Intel. Trenutno deluje zgolj na določeni strojni opremi z HDR10 videi. Ima prednost pred drugimi OpenCL implementacijami.", "TonemappingAlgorithmHelp": "Preslikavo barv lahko podrobno nastavite. Če teh možnosti ne poznate, pustite privzete vrednosti. Priporočena vrednost je 'BT.2390'.", "LabelTonemappingThresholdHelp": "Parametri preslikave barv so natančno nastavljeni za vsak prizor. Prag je uporabljen za zaznavanje ali se je prizor spremenil ali ne. Če je razlika med povprečno svetlostjo trenutne sličice in tekočim povprečjem večja od nastavljenega praga, se vrednosti za povprečno in najvišjo svetlost prizora znova izračunajo. Priporočena in privzeta vrednost je 0,8 in 0,2.", @@ -1693,11 +1693,11 @@ "PreferEmbeddedExtrasTitlesOverFileNames": "Raje uporabi vdelane naslove kot imena datotek", "SaveRecordingNFOHelp": "Shrani metapodatke v isto mapo.", "LabelDummyChapterDuration": "interval", - "LabelDummyChapterDurationHelp": "Interval ekstrakcije slike poglavja v sekundah.", + "LabelDummyChapterDurationHelp": "Interval med navideznimi poglavji. Nastavite na 0 za onemogočanje ustvarjanja navideznih poglavij. Sprememba ne bo imela vpliva na obstoječa navidezna poglavja.", "LabelDummyChapterCount": "Limit", "LabelDummyChapterCountHelp": "Največje število slik poglavij, ki bodo ekstrahirane za vsako medijsko datoteko.", "LabelChapterImageResolution": "Resolucija", - "LabelChapterImageResolutionHelp": "Ločljivost slik poglavij.", + "LabelChapterImageResolutionHelp": "Ločljivost slik poglavij. Spreminjanje ne bo vplivalo na obstoječa lažna poglavja.", "ResolutionMatchSource": "ujemanje z virom", "HeaderRecordingMetadataSaving": "Snemanje metapodatkov", "AllowCollectionManagement": "Dovoli uporabniku upravljanje zbirk", @@ -1714,5 +1714,61 @@ "LabelSegmentKeepSeconds": "Čas ohranitve segmentov", "AllowSegmentDeletionHelp": "Izbriši segmente, ki so če bili poslani odjemalcu. S tem se prepreči, da bi na disku bila shranjena celotna prekodirana datoteka. To deluje le, če je omogočeno zaviranje prekodiranja. Če se pojavijo težave pri predvajanju, onemogočite to možnost.", "LabelThrottleDelaySecondsHelp": "Čas v sekundah, po katerem se bo prekodirnik upočasnil. Mora biti dovolj, da odjemalec ohranja ustrezen medpomnilnik. Deluje le, če je zaviranje prekodiranja omogočeno.", - "LabelSegmentKeepSecondsHelp": "Čas v skundah, ko naj se segmenti ohranijo, preden se jih prepiše. Čas mora biti večji od \"Zaviraj po\". Deluje le, če je brisanje segmentov omogočeno." + "LabelSegmentKeepSecondsHelp": "Čas v skundah, ko naj se segmenti ohranijo, preden se jih prepiše. Čas mora biti večji od \"Zaviraj po\". Deluje le, če je brisanje segmentov omogočeno.", + "MessageRepositoryInstallDisclaimer": "POZOR: Nameščanje skladišča dodatkov tretjih oseb predstavlja tveganje. Lahko vsebuje nestabilno ali zlonamerno kodo, ki se lahko kadarkoli spremeni. Namestite zgolj skladišča avtorjev ki jim zaupate.", + "Studio": "Studio", + "TonemappingModeHelp": "Izberi način preslikave barv. Če opazite razbarvanje svetlih delov slike, poskusite uporabiti način RGB.", + "LabelIsHearingImpaired": "Za naglušne (SDH)", + "AllowAv1Encoding": "Dovoli kodiranje v AV1 format", + "BackdropScreensaver": "Ozadje ohranjevalnika zaslona", + "LogoScreensaver": "Ohranjevalnik zaslona z logotipom", + "Short": "Kratki film", + "PasswordRequiredForAdmin": "Administratorski računi zahtevajo geslo.", + "SaveRecordingImages": "Shrani posnetke slik EPG", + "SubtitleBlack": "Črna", + "SubtitleRed": "Rdeča", + "LabelSyncPlayNoGroups": "Nobena skupina ni na voljo", + "NotificationsMovedMessage": "Funkcionalnost obvestil se je premaknila v dodatek Webhook.", + "SecondarySubtitles": "Sekundarni podnapisi", + "LabelDeveloper": "Razvijalec", + "LabelEnableAudioVbrHelp": "Spremenljiva bitna hitrost omogoča boljše razmerje med kvaliteto zvoka in povprečno bitno hitrostjo vendar lahko povzroči težave z medpomnjenjem in kompatibilnostjo.", + "LogLevel.None": "Brez", + "ForeignPartsOnly": "Samo vsiljeni/tuji deli", + "MachineTranslated": "Strojno prevedeno", + "LabelDate": "Datum", + "LabelEnableLUFSScan": "Omogoči skeniranje LUFS", + "LabelEnableLUFSScanHelp": "Odjemalci lahko normalizirajo glasnost zvoka za enako glasnost med skladbami. Pregledovanje knjižnice bo počasnejše in bo porabilo več sistemskih virov.", + "LabelLevel": "Nivo", + "LabelSystem": "Sistem", + "LogLevel.Trace": "", + "LogLevel.Warning": "Opozorilo", + "LogLevel.Information": "Informacije", + "Select": "Izberi", + "LogLevel.Error": "Napaka", + "LogLevel.Critical": "Kritično", + "GoHome": "Domov", + "LabelMediaDetails": "Podrobnosti predstavnosti", + "SubtitleWhite": "Bela", + "UnknownError": "Prišlo je do neznane napake.", + "Unknown": "Neznano", + "LabelBackdropScreensaverInterval": "Interval ozadja ohranjevalnika zaslona", + "LabelBackdropScreensaverIntervalHelp": "Čas v sekundah med različnimi ozadji za ohranjevalnik zaslona.", + "Notifications": "Obvestila", + "LabelEnableAudioVbr": "Omogoči VBR kodiranje zvoka", + "ListView": "Pogled seznama", + "PleaseConfirmRepositoryInstallation": "S klikom na OK potrjujete, da ste prebrali zgornje opozorilo in želite nadaljevati z namestitvijo skladišča dodatkov.", + "MenuOpen": "Odpri meni", + "MenuClose": "Zapri meni", + "UserMenu": "Uporabniški meni", + "LabelTonemappingMode": "Način preslikave barv", + "LabelParallelImageEncodingLimit": "Omejitev vzporednega kodiranja slik", + "SubtitleYellow": "Rumena", + "SubtitleBlue": "Modra", + "SubtitleCyan": "Cian", + "SubtitleGray": "Siva", + "SubtitleGreen": "Zelena", + "SubtitleLightGray": "Svetlo siva", + "SubtitleMagenta": "Magenta", + "LabelParallelImageEncodingLimitHelp": "Največje dovoljeno število vzporednih kodiranj slik. Nastavite na 0 za samodejno omejitev glede na zmogljivost vašega sistema.", + "AiTranslated": "AI prevedeno" } From 10db65500e7847b5f30d0d80b76ab64977e7ef39 Mon Sep 17 00:00:00 2001 From: krvi Date: Mon, 9 Oct 2023 20:45:09 +0000 Subject: [PATCH 08/31] Translated using Weblate (Faroese) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fo/ --- src/strings/fo.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/strings/fo.json b/src/strings/fo.json index 6532f3e13a..c98afcf767 100644 --- a/src/strings/fo.json +++ b/src/strings/fo.json @@ -212,5 +212,8 @@ "ValueMusicVideoCount": "{0} tónleika sjónbond", "ValueOneAlbum": "1 album", "ValueOneSong": "1 sangur", - "ValueSongCount": "{0} sangir" + "ValueSongCount": "{0} sangir", + "ButtonForgotPassword": "Gloymt loyniorð", + "ButtonSignIn": "Innrita", + "ButtonSignOut": "Útrita" } From e0f723a13a7e0e759450908dd5697ffaccf6425c Mon Sep 17 00:00:00 2001 From: hoanghuy309 Date: Tue, 10 Oct 2023 02:11:39 +0000 Subject: [PATCH 09/31] Translated using Weblate (Vietnamese) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/vi/ --- src/strings/vi.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/strings/vi.json b/src/strings/vi.json index a53d197c41..693eb272a5 100644 --- a/src/strings/vi.json +++ b/src/strings/vi.json @@ -1,6 +1,6 @@ { "Add": "Thêm", - "All": "Tất cả", + "All": "Tất cả", "MessageBrowsePluginCatalog": "Duyệt danh mục plugin của chúng tôi để xem các plugin có sẵn.", "ButtonAddUser": "Thêm Người Dùng", "ButtonCancel": "Hủy bỏ", From a318b1d395408bb81faac79908ebbf9e52ee51f4 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Tue, 10 Oct 2023 00:39:31 -0400 Subject: [PATCH 10/31] Fix PR publish in GH actions --- .github/workflows/build.yml | 24 ++++++++++++++++++++++++ .github/workflows/publish.yml | 2 ++ 2 files changed, 26 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e6d71a2408..0a94de4264 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,3 +39,27 @@ jobs: name: jellyfin-web__prod path: | dist + + pr_context: + name: Save PR context as artifact + if: ${{ always() && !cancelled() && github.event_name == 'pull_request' }} + runs-on: ubuntu-latest + needs: + - run-build-prod + + steps: + - name: Save PR context + env: + PR_NUMBER: ${{ github.event.number }} + PR_SHA: ${{ github.sha }} + run: | + echo $PR_NUMBER > PR_number + echo $PR_SHA > PR_sha + + - name: Upload PR number as artifact + uses: actions/upload-artifact@v3.1.3 + with: + name: PR_context + path: | + PR_number + PR_sha diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1fcf65711e..96eaef368e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,3 +1,5 @@ +name: Publish + on: workflow_run: workflows: From bab1fd7bb9e126c1b179fad9f49f9b161715fb40 Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo Date: Wed, 4 Oct 2023 19:33:09 +0300 Subject: [PATCH 11/31] Fix FilterMenu not saving simple filters Regression 6341a71fec882726bdffa4ed3460a6cbcf252d6e --- src/components/filtermenu/filtermenu.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/filtermenu/filtermenu.js b/src/components/filtermenu/filtermenu.js index d7e9c87650..324dca2316 100644 --- a/src/components/filtermenu/filtermenu.js +++ b/src/components/filtermenu/filtermenu.js @@ -103,7 +103,7 @@ function onInputCommand(e) { } } function saveValues(context, settings, settingsKey, setfilters) { - let elems = context.querySelectorAll('.simpleFilter'); + let elems; // Video type const videoTypes = []; @@ -157,6 +157,8 @@ function saveValues(context, settings, settingsKey, setfilters) { GenreIds: genres.join(',') })); } else { + elems = context.querySelectorAll('.simpleFilter'); + for (let i = 0, length = elems.length; i < length; i++) { if (elems[i].tagName === 'INPUT') { setBasicFilter(context, settingsKey + '-filter-' + elems[i].getAttribute('data-settingname'), elems[i]); From b81342d23b11f28871a46a2752d53f8dde02ac77 Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo Date: Wed, 4 Oct 2023 00:09:31 +0300 Subject: [PATCH 12/31] refactor: Move queries into loops --- src/components/filtermenu/filtermenu.js | 36 +++++++++---------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/src/components/filtermenu/filtermenu.js b/src/components/filtermenu/filtermenu.js index 324dca2316..aa6bd5abe3 100644 --- a/src/components/filtermenu/filtermenu.js +++ b/src/components/filtermenu/filtermenu.js @@ -103,35 +103,27 @@ function onInputCommand(e) { } } function saveValues(context, settings, settingsKey, setfilters) { - let elems; - // Video type const videoTypes = []; - elems = context.querySelectorAll('.chkVideoTypeFilter'); - - for (let i = 0, length = elems.length; i < length; i++) { - if (elems[i].checked) { - videoTypes.push(elems[i].getAttribute('data-filter')); + for (const elem of context.querySelectorAll('.chkVideoTypeFilter')) { + if (elem.checked) { + videoTypes.push(elem.getAttribute('data-filter')); } } // Series status const seriesStatuses = []; - elems = context.querySelectorAll('.chkSeriesStatus'); - - for (let i = 0, length = elems.length; i < length; i++) { - if (elems[i].checked) { - seriesStatuses.push(elems[i].getAttribute('data-filter')); + for (const elem of context.querySelectorAll('.chkSeriesStatus')) { + if (elem.checked) { + seriesStatuses.push(elem.getAttribute('data-filter')); } } // Genres const genres = []; - elems = context.querySelectorAll('.chkGenreFilter'); - - for (let i = 0, length = elems.length; i < length; i++) { - if (elems[i].checked) { - genres.push(elems[i].getAttribute('data-filter')); + for (const elem of context.querySelectorAll('.chkGenreFilter')) { + if (elem.checked) { + genres.push(elem.getAttribute('data-filter')); } } @@ -157,13 +149,11 @@ function saveValues(context, settings, settingsKey, setfilters) { GenreIds: genres.join(',') })); } else { - elems = context.querySelectorAll('.simpleFilter'); - - for (let i = 0, length = elems.length; i < length; i++) { - if (elems[i].tagName === 'INPUT') { - setBasicFilter(context, settingsKey + '-filter-' + elems[i].getAttribute('data-settingname'), elems[i]); + for (const elem of context.querySelectorAll('.simpleFilter')) { + if (elem.tagName === 'INPUT') { + setBasicFilter(context, settingsKey + '-filter-' + elem.getAttribute('data-settingname'), elem); } else { - setBasicFilter(context, settingsKey + '-filter-' + elems[i].getAttribute('data-settingname'), elems[i].querySelector('input')); + setBasicFilter(context, settingsKey + '-filter-' + elem.getAttribute('data-settingname'), elem.querySelector('input')); } } From fc37f82e525b85fcbee922fef970388fcaf4dc1a Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo Date: Tue, 10 Oct 2023 09:40:55 +0300 Subject: [PATCH 13/31] refactor: Use forEach for portability --- src/components/filtermenu/filtermenu.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/filtermenu/filtermenu.js b/src/components/filtermenu/filtermenu.js index aa6bd5abe3..5c78a6f4a4 100644 --- a/src/components/filtermenu/filtermenu.js +++ b/src/components/filtermenu/filtermenu.js @@ -105,27 +105,27 @@ function onInputCommand(e) { function saveValues(context, settings, settingsKey, setfilters) { // Video type const videoTypes = []; - for (const elem of context.querySelectorAll('.chkVideoTypeFilter')) { + context.querySelectorAll('.chkVideoTypeFilter').forEach(elem => { if (elem.checked) { videoTypes.push(elem.getAttribute('data-filter')); } - } + }); // Series status const seriesStatuses = []; - for (const elem of context.querySelectorAll('.chkSeriesStatus')) { + context.querySelectorAll('.chkSeriesStatus').forEach(elem => { if (elem.checked) { seriesStatuses.push(elem.getAttribute('data-filter')); } - } + }); // Genres const genres = []; - for (const elem of context.querySelectorAll('.chkGenreFilter')) { + context.querySelectorAll('.chkGenreFilter').forEach(elem => { if (elem.checked) { genres.push(elem.getAttribute('data-filter')); } - } + }); if (setfilters) { setfilters((prevState) => ({ @@ -149,13 +149,13 @@ function saveValues(context, settings, settingsKey, setfilters) { GenreIds: genres.join(',') })); } else { - for (const elem of context.querySelectorAll('.simpleFilter')) { + context.querySelectorAll('.simpleFilter').forEach(elem => { if (elem.tagName === 'INPUT') { setBasicFilter(context, settingsKey + '-filter-' + elem.getAttribute('data-settingname'), elem); } else { setBasicFilter(context, settingsKey + '-filter-' + elem.getAttribute('data-settingname'), elem.querySelector('input')); } - } + }); userSettings.setFilter(settingsKey + '-filter-GenreIds', genres.join(',')); } From 54d9d90e849ae458dc9af88ccdc92b67fb98bd11 Mon Sep 17 00:00:00 2001 From: RNGIllSkillz Date: Tue, 10 Oct 2023 14:31:55 +0000 Subject: [PATCH 14/31] Translated using Weblate (Russian) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ru/ --- src/strings/ru.json | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/strings/ru.json b/src/strings/ru.json index d584214bd0..070ad5b791 100644 --- a/src/strings/ru.json +++ b/src/strings/ru.json @@ -1401,7 +1401,7 @@ "QuickConnectDescription": "Чтобы войти в систему с помощью Быстрого подключения, нажмите кнопку «Быстрое подключение» на устройстве, с которого вы выполняете вход, и введите указанный ниже код.", "QuickConnectDeactivated": "Быстрое подключение было деактивировано до утверждения запроса на вход", "QuickConnectAuthorizeFail": "Неопознанный код Быстрого подключения", - "QuickConnectAuthorizeSuccess": "Запрос авторизован", + "QuickConnectAuthorizeSuccess": "Вы успешно авторизовали ваше устройство!", "QuickConnectAuthorizeCode": "Введите код {0} для входа", "QuickConnectActivationSuccessful": "Активировано успешно", "QuickConnect": "Быстрое подключение", @@ -1424,7 +1424,7 @@ "LabelTonemappingPeakHelp": "Этим значением перекрывается сигнальный/номинальный/эталонный пик. Полезно, когда встроенная информация о пиках в метаданных дисплея ненадёжна или при тонмаппинге из узкого диапазона в более широкий. Значения рекомендуемое и по умолчанию - 100 и 0.", "LabelTonemappingThresholdHelp": "Параметры алгоритма тонмаппинга подстраиваются для каждой сцены. А порог используется, чтобы определить, изменилась ли сцена или нет. Если дистанция между средней яркостью текущего кадра и текущим скользящим средним превышает пороговое значение, мы пересчитаем среднюю и пиковую яркость сцены. Значения рекомендуемое и по умолчанию - 0.8 и 0.2.", "TonemappingAlgorithmHelp": "Тонмаппинг можно подстроить. Если вы не уверены с этими параметрами, оставьте значения по умолчанию. Рекомендуемое значение - \"BT.2390\".", - "AllowTonemappingHelp": "Тонмаппинг может преобразовать динамический диапазон видео из HDR в SDR, сохраняя детали изображения и цвета, которые являются очень важной информацией для представления исходной сцены. В настоящее время работает только с 10-бит HDR10, HLGи DoVi видео. Для этого требуется соответствующая среда выполнения OpenCL или CUDA.", + "AllowTonemappingHelp": "Тонмаппинг может преобразовать динамический диапазон видео из HDR в SDR, сохраняя детали изображения и цвета, которые являются очень важной информацией для представления исходного видео. В настоящее время работает только с 10-бит HDR10, HLG и DoVi. Для этого требуется соответствующая среда выполнения OpenCL или CUDA.", "LabelOpenclDeviceHelp": "Это устройство OpenCL, которое используется для тонмаппинга. Слева от точки - номер платформы, а справа - это номер устройства на платформе. Значение по умолчанию - 0.0. Требуется файл приложения FFmpeg, содержащий метод аппаратного ускорения OpenCL.", "OptionAllowContentDownloadHelp": "Пользователи могут загружать медиафайлы и хранить их на своих устройствах. Это не то же самое, как функция синхронизации. Для правильной работы книжных медиатек это необходимо.", "HeaderDeleteDevices": "Удалить все устройства", @@ -1742,7 +1742,7 @@ "LogLevel.Critical": "Критично", "LogLevel.None": "Ничего", "HeaderEpisodesStatus": "Статус эпизодов", - "LabelEnableLUFSScanHelp": "Включите сканирование LUFS для музыки (это займет больше времени и ресурсов).", + "LabelEnableLUFSScanHelp": "Клиенты могут нормализировать воспроизведения звука, чтобы обеспечить одинаковую громкость в разных треках. Сканированния библеотек займет больше времени и ресурсов.", "LabelLevel": "Уровень", "LogLevel.Debug": "Отладка", "MessageRepositoryInstallDisclaimer": "ПРЕДУПРЕЖДЕНИЕ: Установка стороннего репозитория плагинов несет определенные риски. Он может содержать нестабильный или вредоносный код и может изменяться в любое время. Устанавливайте репозитории только от авторов, которым вы доверяете.", @@ -1764,7 +1764,17 @@ "LogLevel.Error": "Ошибка", "LabelBackdropScreensaverInterval": "Интервал между фонами у заставки", "LabelBackdropScreensaverIntervalHelp": "Время в секундах между разными фонами, когда используется заставка.", - "BackdropScreensaver": "Заставка", + "BackdropScreensaver": "Фоновая заставка", "GoHome": "Домой", - "GridView": "Сеткой" + "GridView": "Отображение сеткой", + "LabelIsHearingImpaired": "Для людей со слабым слухом (SDH)", + "AllowAv1Encoding": "Разрешить кодирование AV1 формата", + "LogoScreensaver": "Логотип заставки", + "HeaderGuestCast": "Приглашенные звезды", + "UnknownError": "Возникла не известная ошибка.", + "ListView": "Отображение списком", + "AiTranslated": "Переведено при помощи ИИ", + "MachineTranslated": "Машинный перевод", + "HearingImpairedShort": "HI/SDH", + "ForeignPartsOnly": "Только для принудительных и иностранных частей" } From fbcd543a2629461004719d8c1811d749f299deb2 Mon Sep 17 00:00:00 2001 From: scampower3 <81431263+scampower3@users.noreply.github.com> Date: Wed, 11 Oct 2023 01:49:41 +0800 Subject: [PATCH 15/31] Fix Same year appears in - format under card. --- src/components/cardbuilder/cardBuilder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/cardbuilder/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js index ef44a1c33f..6d33700ca0 100644 --- a/src/components/cardbuilder/cardBuilder.js +++ b/src/components/cardbuilder/cardBuilder.js @@ -669,7 +669,7 @@ function getCardFooterText(item, apiClient, options, footerClass, progressHtml, lines.push(globalize.translate('SeriesYearToPresent', productionYear || '')); } else if (item.EndDate && item.ProductionYear) { const endYear = datetime.toLocaleString(datetime.parseISO8601Date(item.EndDate).getFullYear(), { useGrouping: false }); - lines.push(productionYear + ((endYear === item.ProductionYear) ? '' : (' - ' + endYear))); + lines.push(productionYear + ((endYear === productionYear) ? '' : (' - ' + endYear))); } else { lines.push(productionYear || ''); } From 21f2d7ccdf44057170f2760397864f88c96ae675 Mon Sep 17 00:00:00 2001 From: scampower3 <81431263+scampower3@users.noreply.github.com> Date: Wed, 11 Oct 2023 01:50:43 +0800 Subject: [PATCH 16/31] Added myself to CONTRIBUTORS.md --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 920fd92885..41fb667ae8 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -67,6 +67,7 @@ - [sleepycatcoding](https://github.com/sleepycatcoding) - [TheMelmacian](https://github.com/TheMelmacian) - [tehciolo](https://github.com/tehciolo) + - [scampower3](https://github.com/scampower3) # Emby Contributors From f173cf447d4d18df839be52e9c379ff985cca01a Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Tue, 10 Oct 2023 16:45:59 -0400 Subject: [PATCH 17/31] Add current branch to CF Pages publish job --- .github/workflows/publish.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 96eaef368e..c0e690e737 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -33,6 +33,7 @@ jobs: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} projectName: jellyfin-web + branch: ${{ github.event.workflow_run.head_branch }} directory: dist gitHubToken: ${{ secrets.GITHUB_TOKEN }} From 11c191d56b5744f54f9c7f9e607f8e90c9ff1265 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Tue, 10 Oct 2023 16:25:11 -0400 Subject: [PATCH 18/31] Fix libraries maybe not being displayed on tvs --- src/components/homesections/homesections.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/components/homesections/homesections.js b/src/components/homesections/homesections.js index 83b8ec551d..708fcb04b9 100644 --- a/src/components/homesections/homesections.js +++ b/src/components/homesections/homesections.js @@ -1,3 +1,4 @@ +import layoutManager from 'components/layoutManager'; import globalize from 'scripts/globalize'; import { DEFAULT_SECTIONS, HomeSectionType } from 'types/homeSectionType'; import Dashboard from 'utils/dashboard'; @@ -33,6 +34,18 @@ function getAllSectionsToShow(userSettings, sectionCount) { sections.push(section); } + // Ensure libraries are visible in TV layout + if ( + layoutManager.tv + && !sections.includes(HomeSectionType.SmallLibraryTiles) + && !sections.includes(HomeSectionType.LibraryButtons) + ) { + return [ + HomeSectionType.SmallLibraryTiles, + ...sections + ]; + } + return sections; } @@ -41,8 +54,10 @@ export function loadSections(elem, apiClient, user, userSettings) { let html = ''; if (userViews.length) { - const sectionCount = 7; - for (let i = 0; i < sectionCount; i++) { + const userSectionCount = 7; + // TV layout can have an extra section to ensure libraries are visible + const totalSectionCount = layoutManager.tv ? userSectionCount + 1 : userSectionCount; + for (let i = 0; i < totalSectionCount; i++) { html += '
'; } @@ -50,7 +65,7 @@ export function loadSections(elem, apiClient, user, userSettings) { elem.classList.add('homeSectionsContainer'); const promises = []; - const sections = getAllSectionsToShow(userSettings, sectionCount); + const sections = getAllSectionsToShow(userSettings, userSectionCount); for (let i = 0; i < sections.length; i++) { promises.push(loadSection(elem, apiClient, user, userSettings, userViews, sections, i)); } From c61df2eb9234fa829b2f5b263269bd138b9abfba Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Wed, 4 Oct 2023 23:14:14 +0300 Subject: [PATCH 19/31] Refactor: viewitemcontainer --- .../library/GenresItemsContainer.tsx | 5 +- .../library/GenresSectionContainer.tsx | 5 +- .../components/library}/ItemsContainer.tsx | 14 +- .../components/library/ItemsView.tsx | 272 ++++++++++++ .../components/library/Pagination.tsx | 34 +- .../library/RecommendationContainer.tsx | 2 +- .../library/SuggestionsItemsContainer.tsx | 3 +- .../library/SuggestionsSectionContainer.tsx | 5 +- .../library/filter/FilterButton.tsx | 13 +- src/apps/experimental/routes/home.tsx | 2 +- .../routes/movies/CollectionsView.tsx | 32 +- .../routes/movies/FavoritesView.tsx | 28 +- .../experimental/routes/movies/MoviesView.tsx | 30 +- .../routes/movies/TrailersView.tsx | 29 +- src/apps/experimental/routes/movies/index.tsx | 7 +- .../common/AlphaPickerContainer.tsx | 59 --- src/components/common/Filter.tsx | 65 --- src/components/common/NewCollection.tsx | 42 -- src/components/common/Pagination.tsx | 97 ----- src/components/common/SelectView.tsx | 54 --- src/components/common/Shuffle.tsx | 45 -- src/components/common/Sort.tsx | 58 --- src/components/common/ViewItemsContainer.tsx | 411 ------------------ src/hooks/useFetchItems.ts | 167 ++++++- src/types/cardOptions.ts | 3 +- src/types/interface.ts | 29 -- src/types/library.ts | 2 +- src/utils/items.ts | 8 +- 28 files changed, 520 insertions(+), 1001 deletions(-) rename src/{components/common => apps/experimental/components/library}/ItemsContainer.tsx (58%) create mode 100644 src/apps/experimental/components/library/ItemsView.tsx delete mode 100644 src/components/common/AlphaPickerContainer.tsx delete mode 100644 src/components/common/Filter.tsx delete mode 100644 src/components/common/NewCollection.tsx delete mode 100644 src/components/common/Pagination.tsx delete mode 100644 src/components/common/SelectView.tsx delete mode 100644 src/components/common/Shuffle.tsx delete mode 100644 src/components/common/Sort.tsx delete mode 100644 src/components/common/ViewItemsContainer.tsx delete mode 100644 src/types/interface.ts diff --git a/src/apps/experimental/components/library/GenresItemsContainer.tsx b/src/apps/experimental/components/library/GenresItemsContainer.tsx index 41fba412d1..0348beb943 100644 --- a/src/apps/experimental/components/library/GenresItemsContainer.tsx +++ b/src/apps/experimental/components/library/GenresItemsContainer.tsx @@ -5,10 +5,11 @@ import globalize from 'scripts/globalize'; import Loading from 'components/loading/LoadingComponent'; import GenresSectionContainer from './GenresSectionContainer'; import { CollectionType } from 'types/collectionType'; +import { ParentId } from 'types/library'; interface GenresItemsContainerProps { - parentId?: string | null; - collectionType?: CollectionType; + parentId: ParentId; + collectionType: CollectionType; itemType: BaseItemKind; } diff --git a/src/apps/experimental/components/library/GenresSectionContainer.tsx b/src/apps/experimental/components/library/GenresSectionContainer.tsx index 74f57782b1..ebfd20d33d 100644 --- a/src/apps/experimental/components/library/GenresSectionContainer.tsx +++ b/src/apps/experimental/components/library/GenresSectionContainer.tsx @@ -12,10 +12,11 @@ import Loading from 'components/loading/LoadingComponent'; import { appRouter } from 'components/router/appRouter'; import SectionContainer from './SectionContainer'; import { CollectionType } from 'types/collectionType'; +import { ParentId } from 'types/library'; interface GenresSectionContainerProps { - parentId?: string | null; - collectionType?: CollectionType; + parentId: ParentId; + collectionType: CollectionType; itemType: BaseItemKind; genre: BaseItemDto; } diff --git a/src/components/common/ItemsContainer.tsx b/src/apps/experimental/components/library/ItemsContainer.tsx similarity index 58% rename from src/components/common/ItemsContainer.tsx rename to src/apps/experimental/components/library/ItemsContainer.tsx index 6289c1d811..4c3b28c71c 100644 --- a/src/components/common/ItemsContainer.tsx +++ b/src/apps/experimental/components/library/ItemsContainer.tsx @@ -1,16 +1,16 @@ import React, { FC, useEffect, useRef } from 'react'; -import ItemsContainerElement from '../../elements/ItemsContainerElement'; -import imageLoader from '../images/imageLoader'; -import '../../elements/emby-itemscontainer/emby-itemscontainer'; -import { ViewQuerySettings } from '../../types/interface'; +import ItemsContainerElement from 'elements/ItemsContainerElement'; +import imageLoader from 'components/images/imageLoader'; +import 'elements/emby-itemscontainer/emby-itemscontainer'; +import { LibraryViewSettings, ViewMode } from 'types/library'; interface ItemsContainerI { - viewQuerySettings: ViewQuerySettings; + libraryViewSettings: LibraryViewSettings; getItemsHtml: () => string } -const ItemsContainer: FC = ({ viewQuerySettings, getItemsHtml }) => { +const ItemsContainer: FC = ({ libraryViewSettings, getItemsHtml }) => { const element = useRef(null); useEffect(() => { @@ -19,7 +19,7 @@ const ItemsContainer: FC = ({ viewQuerySettings, getItemsHtml } imageLoader.lazyChildren(itemsContainer); }, [getItemsHtml]); - const cssClass = viewQuerySettings.imageType == 'list' ? 'vertical-list' : 'vertical-wrap'; + const cssClass = libraryViewSettings.ViewMode === ViewMode.ListView ? 'vertical-list' : 'vertical-wrap'; return (
diff --git a/src/apps/experimental/components/library/ItemsView.tsx b/src/apps/experimental/components/library/ItemsView.tsx new file mode 100644 index 0000000000..bdbb54fc77 --- /dev/null +++ b/src/apps/experimental/components/library/ItemsView.tsx @@ -0,0 +1,272 @@ +import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import { ImageType } from '@jellyfin/sdk/lib/generated-client'; +import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by'; +import React, { FC, useCallback } from 'react'; +import Box from '@mui/material/Box'; +import { useLocalStorage } from 'hooks/useLocalStorage'; +import { useGetItem, useGetItemsViewByType } from 'hooks/useFetchItems'; +import { getDefaultLibraryViewSettings, getSettingsKey } from 'utils/items'; +import Loading from 'components/loading/LoadingComponent'; +import listview from 'components/listview/listview'; +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import { playbackManager } from 'components/playback/playbackmanager'; +import globalize from 'scripts/globalize'; +import AlphabetPicker from './AlphabetPicker'; +import FilterButton from './filter/FilterButton'; +import ItemsContainer from './ItemsContainer'; +import NewCollectionButton from './NewCollectionButton'; +import Pagination from './Pagination'; +import PlayAllButton from './PlayAllButton'; +import QueueButton from './QueueButton'; +import ShuffleButton from './ShuffleButton'; +import SortButton from './SortButton'; +import GridListViewButton from './GridListViewButton'; +import { LibraryViewSettings, ParentId, ViewMode } from 'types/library'; +import { CollectionType } from 'types/collectionType'; +import { LibraryTab } from 'types/libraryTab'; + +import { CardOptions } from 'types/cardOptions'; + +interface ItemsViewProps { + viewType: LibraryTab; + parentId: ParentId; + itemType: BaseItemKind[]; + collectionType?: CollectionType; + isBtnPlayAllEnabled?: boolean; + isBtnQueueEnabled?: boolean; + isBtnShuffleEnabled?: boolean; + isBtnSortEnabled?: boolean; + isBtnFilterEnabled?: boolean; + isBtnNewCollectionEnabled?: boolean; + isBtnGridListEnabled?: boolean; + isAlphabetPickerEnabled?: boolean; + noItemsMessage: string; +} + +const ItemsView: FC = ({ + viewType, + parentId, + collectionType, + isBtnPlayAllEnabled = false, + isBtnQueueEnabled = false, + isBtnShuffleEnabled = false, + isBtnSortEnabled = true, + isBtnFilterEnabled = true, + isBtnNewCollectionEnabled = false, + isBtnGridListEnabled = true, + isAlphabetPickerEnabled = true, + itemType, + noItemsMessage +}) => { + const [libraryViewSettings, setLibraryViewSettings] = + useLocalStorage( + getSettingsKey(viewType, parentId), + getDefaultLibraryViewSettings(viewType) + ); + + const { + isLoading, + data: itemsResult, + isPreviousData + } = useGetItemsViewByType( + viewType, + parentId, + itemType, + libraryViewSettings + ); + const { data: item } = useGetItem(parentId); + + const getCardOptions = useCallback(() => { + let shape; + let preferThumb; + let preferDisc; + let preferLogo; + let lines = libraryViewSettings.ShowTitle ? 2 : 0; + + if (libraryViewSettings.ImageType === ImageType.Banner) { + shape = 'banner'; + } else if (libraryViewSettings.ImageType === ImageType.Disc) { + shape = 'square'; + preferDisc = true; + } else if (libraryViewSettings.ImageType === ImageType.Logo) { + shape = 'backdrop'; + preferLogo = true; + } else if (libraryViewSettings.ImageType === ImageType.Thumb) { + shape = 'backdrop'; + preferThumb = true; + } else { + shape = 'auto'; + } + + const cardOptions: CardOptions = { + shape: shape, + showTitle: libraryViewSettings.ShowTitle, + showYear: libraryViewSettings.ShowYear, + cardLayout: libraryViewSettings.CardLayout, + centerText: true, + context: collectionType, + coverImage: true, + preferThumb: preferThumb, + preferDisc: preferDisc, + preferLogo: preferLogo, + overlayPlayButton: false, + overlayMoreButton: true, + overlayText: !libraryViewSettings.ShowTitle + }; + + if ( + viewType === LibraryTab.Songs + || viewType === LibraryTab.Albums + || viewType === LibraryTab.Episodes + ) { + cardOptions.showParentTitle = libraryViewSettings.ShowTitle; + } else if (viewType === LibraryTab.Artists) { + cardOptions.showYear = false; + lines = 1; + } + + cardOptions.lines = lines; + + return cardOptions; + }, [ + libraryViewSettings.ShowTitle, + libraryViewSettings.ImageType, + libraryViewSettings.ShowYear, + libraryViewSettings.CardLayout, + collectionType, + viewType + ]); + + const getItemsHtml = useCallback(() => { + let html = ''; + + if (libraryViewSettings.ViewMode === ViewMode.ListView) { + html = listview.getListViewHtml({ + items: itemsResult?.Items ?? [], + context: collectionType + }); + } else { + html = cardBuilder.getCardsHtml( + itemsResult?.Items ?? [], + getCardOptions() + ); + } + + if (!itemsResult?.Items?.length) { + html += '
'; + html += '

' + globalize.translate('MessageNothingHere') + '

'; + html += '

' + globalize.translate(noItemsMessage) + '

'; + html += '
'; + } + + return html; + }, [ + libraryViewSettings.ViewMode, + itemsResult?.Items, + collectionType, + getCardOptions, + noItemsMessage + ]); + + const totalRecordCount = itemsResult?.TotalRecordCount ?? 0; + const items = itemsResult?.Items ?? []; + const hasFilters = Object.values(libraryViewSettings.Filters ?? {}).some( + (filter) => !!filter + ); + const hasSortName = libraryViewSettings.SortBy.includes( + ItemSortBy.SortName + ); + + return ( + + + + + {isBtnPlayAllEnabled && ( + + )} + {isBtnQueueEnabled + && item + && playbackManager.canQueue(item) && ( + + )} + {isBtnShuffleEnabled && totalRecordCount > 1 && ( + + )} + {isBtnSortEnabled && ( + + )} + {isBtnFilterEnabled && ( + + )} + {isBtnNewCollectionEnabled && } + {isBtnGridListEnabled && ( + + )} + + + {isAlphabetPickerEnabled && hasSortName && ( + + )} + + {isLoading ? ( + + ) : ( + + )} + + + + + + ); +}; + +export default ItemsView; diff --git a/src/apps/experimental/components/library/Pagination.tsx b/src/apps/experimental/components/library/Pagination.tsx index 3d1026254f..1e6a2ab261 100644 --- a/src/apps/experimental/components/library/Pagination.tsx +++ b/src/apps/experimental/components/library/Pagination.tsx @@ -13,15 +13,17 @@ interface PaginationProps { libraryViewSettings: LibraryViewSettings; setLibraryViewSettings: React.Dispatch>; totalRecordCount: number; + isPreviousData: boolean } const Pagination: FC = ({ libraryViewSettings, setLibraryViewSettings, - totalRecordCount + totalRecordCount, + isPreviousData }) => { const limit = userSettings.libraryPageSize(undefined); - const startIndex = libraryViewSettings.StartIndex || 0; + const startIndex = libraryViewSettings.StartIndex ?? 0; const recordsStart = totalRecordCount ? startIndex + 1 : 0; const recordsEnd = limit ? Math.min(startIndex + limit, totalRecordCount) : @@ -29,23 +31,19 @@ const Pagination: FC = ({ const showControls = limit > 0 && limit < totalRecordCount; const onNextPageClick = useCallback(() => { - if (limit > 0) { - const newIndex = startIndex + limit; - setLibraryViewSettings((prevState) => ({ - ...prevState, - StartIndex: newIndex - })); - } + 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 - })); - } + const newIndex = Math.max(0, startIndex - limit); + setLibraryViewSettings((prevState) => ({ + ...prevState, + StartIndex: newIndex + })); }, [limit, setLibraryViewSettings, startIndex]); return ( @@ -67,7 +65,7 @@ const Pagination: FC = ({ @@ -76,7 +74,7 @@ const Pagination: FC = ({ = totalRecordCount } + disabled={startIndex + limit >= totalRecordCount || isPreviousData } onClick={onNextPageClick} > diff --git a/src/apps/experimental/components/library/RecommendationContainer.tsx b/src/apps/experimental/components/library/RecommendationContainer.tsx index 4ec8102f57..6c8ab5cc53 100644 --- a/src/apps/experimental/components/library/RecommendationContainer.tsx +++ b/src/apps/experimental/components/library/RecommendationContainer.tsx @@ -49,7 +49,7 @@ const RecommendationContainer: FC = ({ return ( { return [ @@ -178,7 +179,7 @@ const getSuggestionsSections = (): Sections[] => { }; interface SuggestionsItemsContainerProps { - parentId?: string | null; + parentId: ParentId; sectionsView: SectionsView[]; } diff --git a/src/apps/experimental/components/library/SuggestionsSectionContainer.tsx b/src/apps/experimental/components/library/SuggestionsSectionContainer.tsx index 4c52d712e1..7c09bf489f 100644 --- a/src/apps/experimental/components/library/SuggestionsSectionContainer.tsx +++ b/src/apps/experimental/components/library/SuggestionsSectionContainer.tsx @@ -7,9 +7,10 @@ import { appRouter } from 'components/router/appRouter'; import SectionContainer from './SectionContainer'; import { Sections } from 'types/suggestionsSections'; +import { ParentId } from 'types/library'; interface SuggestionsSectionContainerProps { - parentId?: string | null; + parentId: ParentId; section: Sections; } @@ -37,7 +38,7 @@ const SuggestionsSectionContainer: FC = ({ return ( ( @@ -73,9 +73,10 @@ const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({ })); interface FilterButtonProps { - parentId: string | null | undefined; - itemType: BaseItemKind; + parentId: ParentId; + itemType: BaseItemKind[]; viewType: LibraryTab; + hasFilters: boolean; libraryViewSettings: LibraryViewSettings; setLibraryViewSettings: React.Dispatch< React.SetStateAction @@ -86,6 +87,7 @@ const FilterButton: FC = ({ parentId, itemType, viewType, + hasFilters, libraryViewSettings, setLibraryViewSettings }) => { @@ -153,16 +155,13 @@ const FilterButton: FC = ({ return viewType === LibraryTab.Episodes; }; - const hasFilters = - Object.values(libraryViewSettings.Filters || {}).some((filter) => !!filter); - return ( diff --git a/src/apps/experimental/routes/home.tsx b/src/apps/experimental/routes/home.tsx index 3ab9c8bed7..837db942b1 100644 --- a/src/apps/experimental/routes/home.tsx +++ b/src/apps/experimental/routes/home.tsx @@ -27,7 +27,7 @@ type ControllerProps = { const Home: FunctionComponent = () => { const [ searchParams ] = useSearchParams(); - const initialTabIndex = parseInt(searchParams.get('tab') || '0', 10); + const initialTabIndex = parseInt(searchParams.get('tab') ?? '0', 10); const tabController = useRef(); const tabControllers = useMemo(() => [], []); diff --git a/src/apps/experimental/routes/movies/CollectionsView.tsx b/src/apps/experimental/routes/movies/CollectionsView.tsx index ef574b916e..82ce015565 100644 --- a/src/apps/experimental/routes/movies/CollectionsView.tsx +++ b/src/apps/experimental/routes/movies/CollectionsView.tsx @@ -1,30 +1,22 @@ -import React, { FC, useCallback } from 'react'; +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import React, { FC } from 'react'; -import ViewItemsContainer from 'components/common/ViewItemsContainer'; +import ItemsView from '../../components/library/ItemsView'; import { LibraryViewProps } from 'types/library'; +import { CollectionType } from 'types/collectionType'; +import { LibraryTab } from 'types/libraryTab'; const CollectionsView: FC = ({ parentId }) => { - const getBasekey = useCallback(() => { - return 'collections'; - }, []); - - const getItemTypes = useCallback(() => { - return ['BoxSet']; - }, []); - - const getNoItemsMessage = useCallback(() => { - return 'MessageNoCollectionsAvailable'; - }, []); - return ( - ); }; diff --git a/src/apps/experimental/routes/movies/FavoritesView.tsx b/src/apps/experimental/routes/movies/FavoritesView.tsx index d22cad6e38..7bb89edb15 100644 --- a/src/apps/experimental/routes/movies/FavoritesView.tsx +++ b/src/apps/experimental/routes/movies/FavoritesView.tsx @@ -1,27 +1,17 @@ -import React, { FC, useCallback } from 'react'; +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import React, { FC } from 'react'; -import ViewItemsContainer from 'components/common/ViewItemsContainer'; +import ItemsView from '../../components/library/ItemsView'; import { LibraryViewProps } from 'types/library'; +import { LibraryTab } from 'types/libraryTab'; const FavoritesView: FC = ({ parentId }) => { - const getBasekey = useCallback(() => { - return 'favorites'; - }, []); - - const getItemTypes = useCallback(() => { - return ['Movie']; - }, []); - - const getNoItemsMessage = useCallback(() => { - return 'MessageNoFavoritesAvailable'; - }, []); - return ( - ); }; diff --git a/src/apps/experimental/routes/movies/MoviesView.tsx b/src/apps/experimental/routes/movies/MoviesView.tsx index 8796c9a711..b09f468db1 100644 --- a/src/apps/experimental/routes/movies/MoviesView.tsx +++ b/src/apps/experimental/routes/movies/MoviesView.tsx @@ -1,28 +1,20 @@ -import React, { FC, useCallback } from 'react'; +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import React, { FC } from 'react'; -import ViewItemsContainer from 'components/common/ViewItemsContainer'; +import ItemsView from '../../components/library/ItemsView'; import { LibraryViewProps } from 'types/library'; +import { CollectionType } from 'types/collectionType'; +import { LibraryTab } from 'types/libraryTab'; const MoviesView: FC = ({ parentId }) => { - const getBasekey = useCallback(() => { - return 'movies'; - }, []); - - const getItemTypes = useCallback(() => { - return ['Movie']; - }, []); - - const getNoItemsMessage = useCallback(() => { - return 'MessageNoItemsAvailable'; - }, []); - return ( - ); }; diff --git a/src/apps/experimental/routes/movies/TrailersView.tsx b/src/apps/experimental/routes/movies/TrailersView.tsx index ff0ff0e73e..6acfd1c8ca 100644 --- a/src/apps/experimental/routes/movies/TrailersView.tsx +++ b/src/apps/experimental/routes/movies/TrailersView.tsx @@ -1,28 +1,17 @@ +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import React, { FC } from 'react'; -import React, { FC, useCallback } from 'react'; - -import ViewItemsContainer from 'components/common/ViewItemsContainer'; +import ItemsView from '../../components/library/ItemsView'; import { LibraryViewProps } from 'types/library'; +import { LibraryTab } from 'types/libraryTab'; const TrailersView: FC = ({ parentId }) => { - const getBasekey = useCallback(() => { - return 'trailers'; - }, []); - - const getItemTypes = useCallback(() => { - return ['Trailer']; - }, []); - - const getNoItemsMessage = useCallback(() => { - return 'MessageNoTrailersFound'; - }, []); - return ( - ); }; diff --git a/src/apps/experimental/routes/movies/index.tsx b/src/apps/experimental/routes/movies/index.tsx index e1a30dfb59..8e74b7a3e4 100644 --- a/src/apps/experimental/routes/movies/index.tsx +++ b/src/apps/experimental/routes/movies/index.tsx @@ -1,13 +1,8 @@ -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 React, { FC } from 'react'; import { useLocation, useSearchParams } from 'react-router-dom'; -import Page from 'components/Page'; import { getDefaultTabIndex } from '../../components/tabs/tabRoutes'; +import Page from 'components/Page'; import CollectionsView from './CollectionsView'; import FavoritesView from './FavoritesView'; import GenresView from './GenresView'; diff --git a/src/components/common/AlphaPickerContainer.tsx b/src/components/common/AlphaPickerContainer.tsx deleted file mode 100644 index 6b7c9a0718..0000000000 --- a/src/components/common/AlphaPickerContainer.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; -import AlphaPicker from '../alphaPicker/alphaPicker'; -import { ViewQuerySettings } from '../../types/interface'; - -interface AlphaPickerContainerProps { - viewQuerySettings: ViewQuerySettings; - setViewQuerySettings: React.Dispatch>; -} - -const AlphaPickerContainer: FC = ({ viewQuerySettings, setViewQuerySettings }) => { - const [ alphaPicker, setAlphaPicker ] = useState(); - const element = useRef(null); - - alphaPicker?.updateControls(viewQuerySettings); - - const onAlphaPickerChange = useCallback((e) => { - const newValue = (e as CustomEvent).detail.value; - let updatedValue: React.SetStateAction; - if (newValue === '#') { - updatedValue = { - NameLessThan: 'A', - NameStartsWith: undefined - }; - } else { - updatedValue = { - NameLessThan: undefined, - NameStartsWith: newValue - }; - } - setViewQuerySettings((prevState) => ({ - ...prevState, - StartIndex: 0, - ...updatedValue - })); - }, [setViewQuerySettings]); - - useEffect(() => { - const alphaPickerElement = element.current; - - setAlphaPicker(new AlphaPicker({ - element: alphaPickerElement, - valueChangeEvent: 'click' - })); - - if (alphaPickerElement) { - alphaPickerElement.addEventListener('alphavaluechanged', onAlphaPickerChange); - } - - return () => { - alphaPickerElement?.removeEventListener('alphavaluechanged', onAlphaPickerChange); - }; - }, [onAlphaPickerChange]); - - return ( -
- ); -}; - -export default AlphaPickerContainer; diff --git a/src/components/common/Filter.tsx b/src/components/common/Filter.tsx deleted file mode 100644 index c3ccdd62f3..0000000000 --- a/src/components/common/Filter.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React, { FC, useCallback, useEffect, useRef } from 'react'; -import IconButtonElement from '../../elements/IconButtonElement'; -import { ViewQuerySettings } from '../../types/interface'; - -interface FilterProps { - topParentId?: string | null; - getItemTypes: () => string[]; - getFilterMenuOptions: () => Record; - getVisibleFilters: () => string[]; - viewQuerySettings: ViewQuerySettings; - setViewQuerySettings: React.Dispatch>; -} - -const Filter: FC = ({ - topParentId, - getItemTypes, - getVisibleFilters, - getFilterMenuOptions, - viewQuerySettings, - setViewQuerySettings -}) => { - const element = useRef(null); - - const showFilterMenu = useCallback(() => { - import('../filtermenu/filtermenu').then(({ default: FilterMenu }) => { - const filterMenu = new FilterMenu(); - filterMenu.show({ - settings: viewQuerySettings, - visibleSettings: getVisibleFilters(), - parentId: topParentId, - itemTypes: getItemTypes(), - serverId: window.ApiClient.serverId(), - filterMenuOptions: getFilterMenuOptions(), - setfilters: setViewQuerySettings - }).catch(() => { - // filter menu closed - }); - }).catch(err => { - console.error('[Filter] failed to load filter menu', err); - }); - }, [viewQuerySettings, getVisibleFilters, topParentId, getItemTypes, getFilterMenuOptions, setViewQuerySettings]); - - useEffect(() => { - const btnFilter = element.current?.querySelector('.btnFilter'); - - btnFilter?.addEventListener('click', showFilterMenu); - - return () => { - btnFilter?.removeEventListener('click', showFilterMenu); - }; - }, [showFilterMenu]); - - return ( -
- -
- ); -}; - -export default Filter; diff --git a/src/components/common/NewCollection.tsx b/src/components/common/NewCollection.tsx deleted file mode 100644 index 837fe85fd3..0000000000 --- a/src/components/common/NewCollection.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React, { FC, useCallback, useEffect, useRef } from 'react'; - -import IconButtonElement from '../../elements/IconButtonElement'; - -const NewCollection: FC = () => { - const element = useRef(null); - - const showCollectionEditor = useCallback(() => { - import('../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); - }); - }, []); - - useEffect(() => { - const btnNewCollection = element.current?.querySelector('.btnNewCollection'); - if (btnNewCollection) { - btnNewCollection.addEventListener('click', showCollectionEditor); - } - }, [showCollectionEditor]); - - return ( -
- -
- ); -}; - -export default NewCollection; diff --git a/src/components/common/Pagination.tsx b/src/components/common/Pagination.tsx deleted file mode 100644 index 3dd5a60ffd..0000000000 --- a/src/components/common/Pagination.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client'; -import React, { FC, useCallback, useEffect, useRef } from 'react'; -import IconButtonElement from '../../elements/IconButtonElement'; -import globalize from '../../scripts/globalize'; -import * as userSettings from '../../scripts/settings/userSettings'; -import { ViewQuerySettings } from '../../types/interface'; - -interface PaginationProps { - viewQuerySettings: ViewQuerySettings; - setViewQuerySettings: React.Dispatch>; - itemsResult?: BaseItemDtoQueryResult; -} - -const Pagination: FC = ({ viewQuerySettings, setViewQuerySettings, itemsResult = {} }) => { - const limit = userSettings.libraryPageSize(undefined); - const totalRecordCount = itemsResult.TotalRecordCount || 0; - const startIndex = viewQuerySettings.StartIndex || 0; - const recordsStart = totalRecordCount ? startIndex + 1 : 0; - const recordsEnd = limit ? Math.min(startIndex + limit, totalRecordCount) : totalRecordCount; - const showControls = limit > 0 && limit < totalRecordCount; - const element = useRef(null); - - const onNextPageClick = useCallback(() => { - if (limit > 0) { - const newIndex = startIndex + limit; - setViewQuerySettings((prevState) => ({ - ...prevState, - StartIndex: newIndex - })); - } - }, [limit, setViewQuerySettings, startIndex]); - - const onPreviousPageClick = useCallback(() => { - if (limit > 0) { - const newIndex = Math.max(0, startIndex - limit); - setViewQuerySettings((prevState) => ({ - ...prevState, - StartIndex: newIndex - })); - } - }, [limit, setViewQuerySettings, startIndex]); - - useEffect(() => { - const btnNextPage = element.current?.querySelector('.btnNextPage') as HTMLButtonElement; - if (btnNextPage) { - if (startIndex + limit >= totalRecordCount) { - btnNextPage.disabled = true; - } else { - btnNextPage.disabled = false; - } - btnNextPage.addEventListener('click', onNextPageClick); - } - - const btnPreviousPage = element.current?.querySelector('.btnPreviousPage') as HTMLButtonElement; - if (btnPreviousPage) { - if (startIndex) { - btnPreviousPage.disabled = false; - } else { - btnPreviousPage.disabled = true; - } - btnPreviousPage.addEventListener('click', onPreviousPageClick); - } - - return () => { - btnNextPage?.removeEventListener('click', onNextPageClick); - btnPreviousPage?.removeEventListener('click', onPreviousPageClick); - }; - }, [totalRecordCount, onNextPageClick, onPreviousPageClick, limit, startIndex]); - - return ( -
-
-
- - {globalize.translate('ListPaging', recordsStart, recordsEnd, totalRecordCount)} - - {showControls && ( - <> - - - - )} -
-
-
- ); -}; - -export default Pagination; diff --git a/src/components/common/SelectView.tsx b/src/components/common/SelectView.tsx deleted file mode 100644 index bfb34555b8..0000000000 --- a/src/components/common/SelectView.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React, { FC, useCallback, useEffect, useRef } from 'react'; -import IconButtonElement from '../../elements/IconButtonElement'; -import { ViewQuerySettings } from '../../types/interface'; - -interface SelectViewProps { - getVisibleViewSettings: () => string[]; - viewQuerySettings: ViewQuerySettings; - setViewQuerySettings: React.Dispatch>; -} - -const SelectView: FC = ({ - getVisibleViewSettings, - viewQuerySettings, - setViewQuerySettings -}) => { - const element = useRef(null); - - const showViewSettingsMenu = useCallback(() => { - import('../viewSettings/viewSettings').then(({ default: ViewSettings }) => { - const viewsettings = new ViewSettings(); - viewsettings.show({ - settings: viewQuerySettings, - visibleSettings: getVisibleViewSettings(), - setviewsettings: setViewQuerySettings - }).catch(() => { - // view settings closed - }); - }).catch(err => { - console.error('[SelectView] failed to load view settings', err); - }); - }, [getVisibleViewSettings, viewQuerySettings, setViewQuerySettings]); - - useEffect(() => { - const btnSelectView = element.current?.querySelector('.btnSelectView') as HTMLButtonElement; - btnSelectView?.addEventListener('click', showViewSettingsMenu); - - return () => { - btnSelectView?.removeEventListener('click', showViewSettingsMenu); - }; - }, [showViewSettingsMenu]); - - return ( -
- -
- ); -}; - -export default SelectView; diff --git a/src/components/common/Shuffle.tsx b/src/components/common/Shuffle.tsx deleted file mode 100644 index 093dc74874..0000000000 --- a/src/components/common/Shuffle.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client'; -import React, { FC, useCallback, useEffect, useRef } from 'react'; - -import { playbackManager } from '../playback/playbackmanager'; -import IconButtonElement from '../../elements/IconButtonElement'; - -interface ShuffleProps { - itemsResult?: BaseItemDtoQueryResult; - topParentId: string | null; -} - -const Shuffle: FC = ({ itemsResult = {}, topParentId }) => { - const element = useRef(null); - - const shuffle = useCallback(() => { - window.ApiClient.getItem( - window.ApiClient.getCurrentUserId(), - topParentId as string - ).then((item) => { - playbackManager.shuffle(item); - }).catch(err => { - console.error('[Shuffle] failed to fetch items', err); - }); - }, [topParentId]); - - useEffect(() => { - const btnShuffle = element.current?.querySelector('.btnShuffle'); - if (btnShuffle) { - btnShuffle.addEventListener('click', shuffle); - } - }, [itemsResult.TotalRecordCount, shuffle]); - - return ( -
- -
- ); -}; - -export default Shuffle; diff --git a/src/components/common/Sort.tsx b/src/components/common/Sort.tsx deleted file mode 100644 index db5cb89956..0000000000 --- a/src/components/common/Sort.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React, { FC, useCallback, useEffect, useRef } from 'react'; -import IconButtonElement from '../../elements/IconButtonElement'; -import { ViewQuerySettings } from '../../types/interface'; - -interface SortProps { - getSortMenuOptions: () => { - name: string; - value: string; - }[]; - viewQuerySettings: ViewQuerySettings; - setViewQuerySettings: React.Dispatch>; -} - -const Sort: FC = ({ - getSortMenuOptions, - viewQuerySettings, - setViewQuerySettings -}) => { - const element = useRef(null); - - const showSortMenu = useCallback(() => { - import('../sortmenu/sortmenu').then(({ default: SortMenu }) => { - const sortMenu = new SortMenu(); - sortMenu.show({ - settings: viewQuerySettings, - sortOptions: getSortMenuOptions(), - setSortValues: setViewQuerySettings - }).catch(() => { - // sort menu closed - }); - }).catch(err => { - console.error('[Sort] failed to load sort menu', err); - }); - }, [getSortMenuOptions, viewQuerySettings, setViewQuerySettings]); - - useEffect(() => { - const btnSort = element.current?.querySelector('.btnSort'); - - btnSort?.addEventListener('click', showSortMenu); - - return () => { - btnSort?.removeEventListener('click', showSortMenu); - }; - }, [showSortMenu]); - - return ( -
- -
- ); -}; - -export default Sort; diff --git a/src/components/common/ViewItemsContainer.tsx b/src/components/common/ViewItemsContainer.tsx deleted file mode 100644 index 1ea5ef9899..0000000000 --- a/src/components/common/ViewItemsContainer.tsx +++ /dev/null @@ -1,411 +0,0 @@ -import { type BaseItemDtoQueryResult, ItemFields, ItemFilter } from '@jellyfin/sdk/lib/generated-client'; -import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; - -import loading from '../loading/loading'; -import * as userSettings from '../../scripts/settings/userSettings'; -import AlphaPickerContainer from './AlphaPickerContainer'; -import Filter from './Filter'; -import ItemsContainer from './ItemsContainer'; -import Pagination from './Pagination'; -import SelectView from './SelectView'; -import Shuffle from './Shuffle'; -import Sort from './Sort'; -import NewCollection from './NewCollection'; -import globalize from '../../scripts/globalize'; -import ServerConnections from '../ServerConnections'; -import { useLocalStorage } from '../../hooks/useLocalStorage'; -import listview from '../listview/listview'; -import cardBuilder from '../cardbuilder/cardBuilder'; - -import { ViewQuerySettings } from '../../types/interface'; -import { CardOptions } from '../../types/cardOptions'; - -interface ViewItemsContainerProps { - topParentId: string | null; - isBtnShuffleEnabled?: boolean; - isBtnFilterEnabled?: boolean; - isBtnNewCollectionEnabled?: boolean; - isAlphaPickerEnabled?: boolean; - getBasekey: () => string; - getItemTypes: () => string[]; - getNoItemsMessage: () => string; -} - -const getDefaultSortBy = () => { - return 'SortName'; -}; - -const getFields = (viewQuerySettings: ViewQuerySettings) => { - const fields: ItemFields[] = [ - ItemFields.BasicSyncInfo, - ItemFields.MediaSourceCount - ]; - - if (viewQuerySettings.imageType === 'primary') { - fields.push(ItemFields.PrimaryImageAspectRatio); - } - - return fields.join(','); -}; - -const getFilters = (viewQuerySettings: ViewQuerySettings) => { - const filters: ItemFilter[] = []; - - if (viewQuerySettings.IsPlayed) { - filters.push(ItemFilter.IsPlayed); - } - - if (viewQuerySettings.IsUnplayed) { - filters.push(ItemFilter.IsUnplayed); - } - - if (viewQuerySettings.IsFavorite) { - filters.push(ItemFilter.IsFavorite); - } - - if (viewQuerySettings.IsResumable) { - filters.push(ItemFilter.IsResumable); - } - - return filters; -}; - -const getVisibleViewSettings = () => { - return [ - 'showTitle', - 'showYear', - 'imageType', - 'cardLayout' - ]; -}; - -const getFilterMenuOptions = () => { - return {}; -}; - -const getVisibleFilters = () => { - return [ - 'IsUnplayed', - 'IsPlayed', - 'IsFavorite', - 'IsResumable', - 'VideoType', - 'HasSubtitles', - 'HasTrailer', - 'HasSpecialFeature', - 'HasThemeSong', - 'HasThemeVideo' - ]; -}; - -const getSortMenuOptions = () => { - return [{ - name: globalize.translate('Name'), - value: 'SortName,ProductionYear' - }, { - name: globalize.translate('OptionRandom'), - value: 'Random' - }, { - name: globalize.translate('OptionImdbRating'), - value: 'CommunityRating,SortName,ProductionYear' - }, { - name: globalize.translate('OptionCriticRating'), - value: 'CriticRating,SortName,ProductionYear' - }, { - name: globalize.translate('OptionDateAdded'), - value: 'DateCreated,SortName,ProductionYear' - }, { - name: globalize.translate('OptionDatePlayed'), - value: 'DatePlayed,SortName,ProductionYear' - }, { - name: globalize.translate('OptionParentalRating'), - value: 'OfficialRating,SortName,ProductionYear' - }, { - name: globalize.translate('OptionPlayCount'), - value: 'PlayCount,SortName,ProductionYear' - }, { - name: globalize.translate('OptionReleaseDate'), - value: 'PremiereDate,SortName,ProductionYear' - }, { - name: globalize.translate('Runtime'), - value: 'Runtime,SortName,ProductionYear' - }]; -}; - -const defaultViewQuerySettings: ViewQuerySettings = { - showTitle: true, - showYear: true, - imageType: 'primary', - viewType: '', - cardLayout: false, - SortBy: getDefaultSortBy(), - SortOrder: 'Ascending', - IsPlayed: false, - IsUnplayed: false, - IsFavorite: false, - IsResumable: false, - Is4K: null, - IsHD: null, - IsSD: null, - Is3D: null, - VideoTypes: '', - SeriesStatus: '', - HasSubtitles: null, - HasTrailer: null, - HasSpecialFeature: null, - HasThemeSong: null, - HasThemeVideo: null, - GenreIds: '', - StartIndex: 0 -}; - -const ViewItemsContainer: FC = ({ - topParentId, - isBtnShuffleEnabled = false, - isBtnFilterEnabled = true, - isBtnNewCollectionEnabled = false, - isAlphaPickerEnabled = true, - getBasekey, - getItemTypes, - getNoItemsMessage -}) => { - const getSettingsKey = useCallback(() => { - return `${topParentId} - ${getBasekey()}`; - }, [getBasekey, topParentId]); - - const [isLoading, setisLoading] = useState(false); - - const [viewQuerySettings, setViewQuerySettings] = useLocalStorage( - `viewQuerySettings - ${getSettingsKey()}`, - defaultViewQuerySettings - ); - - const [ itemsResult, setItemsResult ] = useState({}); - - const element = useRef(null); - - const getContext = useCallback(() => { - const itemType = getItemTypes().join(','); - if (itemType === 'Movie' || itemType === 'BoxSet') { - return 'movies'; - } - - return null; - }, [getItemTypes]); - - const getCardOptions = useCallback(() => { - let shape; - let preferThumb; - let preferDisc; - let preferLogo; - - if (viewQuerySettings.imageType === 'banner') { - shape = 'banner'; - } else if (viewQuerySettings.imageType === 'disc') { - shape = 'square'; - preferDisc = true; - } else if (viewQuerySettings.imageType === 'logo') { - shape = 'backdrop'; - preferLogo = true; - } else if (viewQuerySettings.imageType === 'thumb') { - shape = 'backdrop'; - preferThumb = true; - } else { - shape = 'autoVertical'; - } - - const cardOptions: CardOptions = { - shape: shape, - showTitle: viewQuerySettings.showTitle, - showYear: viewQuerySettings.showYear, - cardLayout: viewQuerySettings.cardLayout, - centerText: true, - context: getContext(), - coverImage: true, - preferThumb: preferThumb, - preferDisc: preferDisc, - preferLogo: preferLogo, - overlayPlayButton: false, - overlayMoreButton: true, - overlayText: !viewQuerySettings.showTitle - }; - - cardOptions.items = itemsResult.Items || []; - - return cardOptions; - }, [ - getContext, - itemsResult.Items, - viewQuerySettings.cardLayout, - viewQuerySettings.imageType, - viewQuerySettings.showTitle, - viewQuerySettings.showYear - ]); - - const getItemsHtml = useCallback(() => { - let html = ''; - - if (viewQuerySettings.imageType === 'list') { - html = listview.getListViewHtml({ - items: itemsResult.Items || [], - context: getContext() - }); - } else { - html = cardBuilder.getCardsHtml(itemsResult.Items || [], getCardOptions()); - } - - if (!itemsResult.Items?.length) { - html += '
'; - html += '

' + globalize.translate('MessageNothingHere') + '

'; - html += '

' + globalize.translate(getNoItemsMessage()) + '

'; - html += '
'; - } - - return html; - }, [getCardOptions, getContext, itemsResult.Items, getNoItemsMessage, viewQuerySettings.imageType]); - - const getQuery = useCallback(() => { - const queryFilters = getFilters(viewQuerySettings); - - let queryIsHD; - - if (viewQuerySettings.IsHD) { - queryIsHD = true; - } - - if (viewQuerySettings.IsSD) { - queryIsHD = false; - } - - return { - SortBy: viewQuerySettings.SortBy, - SortOrder: viewQuerySettings.SortOrder, - IncludeItemTypes: getItemTypes().join(','), - Recursive: true, - Fields: getFields(viewQuerySettings), - ImageTypeLimit: 1, - EnableImageTypes: 'Primary,Backdrop,Banner,Thumb,Disc,Logo', - Limit: userSettings.libraryPageSize(undefined) || undefined, - IsFavorite: getBasekey() === 'favorites' ? true : null, - VideoTypes: viewQuerySettings.VideoTypes, - GenreIds: viewQuerySettings.GenreIds, - Is4K: viewQuerySettings.Is4K ? true : null, - IsHD: queryIsHD, - Is3D: viewQuerySettings.Is3D ? true : null, - HasSubtitles: viewQuerySettings.HasSubtitles ? true : null, - HasTrailer: viewQuerySettings.HasTrailer ? true : null, - HasSpecialFeature: viewQuerySettings.HasSpecialFeature ? true : null, - HasThemeSong: viewQuerySettings.HasThemeSong ? true : null, - HasThemeVideo: viewQuerySettings.HasThemeVideo ? true : null, - Filters: queryFilters.length ? queryFilters.join(',') : null, - StartIndex: viewQuerySettings.StartIndex, - NameLessThan: viewQuerySettings.NameLessThan, - NameStartsWith: viewQuerySettings.NameStartsWith, - ParentId: topParentId - }; - }, [ - viewQuerySettings, - getItemTypes, - getBasekey, - topParentId - ]); - - const fetchData = useCallback(() => { - loading.show(); - - const apiClient = ServerConnections.getApiClient(window.ApiClient.serverId()); - return apiClient.getItems( - apiClient.getCurrentUserId(), - { - ...getQuery() - } - ); - }, [getQuery]); - - const reloadItems = useCallback(() => { - const page = element.current; - - if (!page) { - console.error('Unexpected null reference'); - return; - } - setisLoading(false); - fetchData().then((result) => { - setItemsResult(result); - - window.scrollTo(0, 0); - - import('../../components/autoFocuser').then(({ default: autoFocuser }) => { - autoFocuser.autoFocus(page); - }).catch(err => { - console.error('[ViewItemsContainer] failed to load autofocuser', err); - }); - loading.hide(); - setisLoading(true); - }).catch(err => { - console.error('[ViewItemsContainer] failed to fetch data', err); - }); - }, [fetchData]); - - useEffect(() => { - reloadItems(); - }, [reloadItems]); - - return ( -
-
- - - {isBtnShuffleEnabled && } - - - - - - {isBtnFilterEnabled && } - - {isBtnNewCollectionEnabled && } - -
- - {isAlphaPickerEnabled && } - - {isLoading && } - -
- -
-
- ); -}; - -export default ViewItemsContainer; diff --git a/src/hooks/useFetchItems.ts b/src/hooks/useFetchItems.ts index c0ed89b85c..6ddd77a120 100644 --- a/src/hooks/useFetchItems.ts +++ b/src/hooks/useFetchItems.ts @@ -1,12 +1,11 @@ import type { ItemsApiGetItemsRequest } from '@jellyfin/sdk/lib/generated-client'; -import { AxiosRequestConfig } from 'axios'; - -import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type'; import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields'; import { ItemFilter } from '@jellyfin/sdk/lib/generated-client/models/item-filter'; import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order'; import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by'; +import { getArtistsApi } from '@jellyfin/sdk/lib/utils/api/artists-api'; import { getFilterApi } from '@jellyfin/sdk/lib/utils/api/filter-api'; import { getGenresApi } from '@jellyfin/sdk/lib/utils/api/genres-api'; import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api'; @@ -14,11 +13,14 @@ import { getMoviesApi } from '@jellyfin/sdk/lib/utils/api/movies-api'; import { getStudiosApi } from '@jellyfin/sdk/lib/utils/api/studios-api'; import { getTvShowsApi } from '@jellyfin/sdk/lib/utils/api/tv-shows-api'; import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api/user-library-api'; +import { AxiosRequestConfig } from 'axios'; import { useQuery } from '@tanstack/react-query'; import { JellyfinApiContext, useApi } from './useApi'; +import { getAlphaPickerQuery, getFieldsQuery, getFiltersQuery, getLimitQuery } from 'utils/items'; import { Sections, SectionsViewType } from 'types/suggestionsSections'; -import { ParentId } from 'types/library'; +import { LibraryViewSettings, ParentId } from 'types/library'; +import { LibraryTab } from 'types/libraryTab'; const fetchGetItem = async ( currentApi: JellyfinApiContext, @@ -291,7 +293,7 @@ export const useGetGenres = (itemType: BaseItemKind, parentId: ParentId) => { const fetchGetStudios = async ( currentApi: JellyfinApiContext, parentId: ParentId, - itemType: BaseItemKind, + itemType: BaseItemKind[], options?: AxiosRequestConfig ) => { const { api, user } = currentApi; @@ -299,7 +301,7 @@ const fetchGetStudios = async ( const response = await getStudiosApi(api).getStudios( { userId: user.Id, - includeItemTypes: [itemType], + includeItemTypes: itemType, fields: [ ItemFields.DateCreated, ItemFields.PrimaryImageAspectRatio @@ -316,7 +318,7 @@ const fetchGetStudios = async ( } }; -export const useGetStudios = (parentId: ParentId, itemType: BaseItemKind) => { +export const useGetStudios = (parentId: ParentId, itemType: BaseItemKind[]) => { const currentApi = useApi(); return useQuery({ queryKey: ['Studios', parentId, itemType], @@ -329,7 +331,7 @@ export const useGetStudios = (parentId: ParentId, itemType: BaseItemKind) => { const fetchGetQueryFiltersLegacy = async ( currentApi: JellyfinApiContext, parentId: ParentId, - itemType: BaseItemKind, + itemType: BaseItemKind[], options?: AxiosRequestConfig ) => { const { api, user } = currentApi; @@ -338,7 +340,7 @@ const fetchGetQueryFiltersLegacy = async ( { userId: user.Id, parentId: parentId ?? undefined, - includeItemTypes: [itemType] + includeItemTypes: itemType }, { signal: options?.signal @@ -350,7 +352,7 @@ const fetchGetQueryFiltersLegacy = async ( export const useGetQueryFiltersLegacy = ( parentId: ParentId, - itemType: BaseItemKind + itemType: BaseItemKind[] ) => { const currentApi = useApi(); return useQuery({ @@ -362,3 +364,148 @@ export const useGetQueryFiltersLegacy = ( enabled: !!parentId }); }; + +const fetchGetItemsViewByType = async ( + currentApi: JellyfinApiContext, + viewType: LibraryTab, + parentId: ParentId, + itemType: BaseItemKind[], + libraryViewSettings: LibraryViewSettings, + options?: AxiosRequestConfig +) => { + const { api, user } = currentApi; + if (api && user?.Id) { + let response; + switch (viewType) { + case LibraryTab.AlbumArtists: { + response = await getArtistsApi(api).getAlbumArtists( + { + userId: user.Id, + parentId: parentId ?? undefined, + enableImageTypes: [libraryViewSettings.ImageType, ImageType.Backdrop], + ...getFieldsQuery(viewType, libraryViewSettings), + ...getFiltersQuery(viewType, libraryViewSettings), + ...getLimitQuery(), + ...getAlphaPickerQuery(libraryViewSettings), + sortBy: [libraryViewSettings.SortBy], + sortOrder: [libraryViewSettings.SortOrder], + includeItemTypes: itemType, + startIndex: libraryViewSettings.StartIndex + }, + { + signal: options?.signal + } + ); + break; + } + case LibraryTab.Artists: { + response = await getArtistsApi(api).getArtists( + { + userId: user.Id, + parentId: parentId ?? undefined, + enableImageTypes: [libraryViewSettings.ImageType, ImageType.Backdrop], + ...getFieldsQuery(viewType, libraryViewSettings), + ...getFiltersQuery(viewType, libraryViewSettings), + ...getLimitQuery(), + ...getAlphaPickerQuery(libraryViewSettings), + sortBy: [libraryViewSettings.SortBy], + sortOrder: [libraryViewSettings.SortOrder], + includeItemTypes: itemType, + startIndex: libraryViewSettings.StartIndex + }, + { + signal: options?.signal + } + ); + break; + } + case LibraryTab.Networks: + response = await getStudiosApi(api).getStudios( + { + userId: user.Id, + parentId: parentId ?? undefined, + ...getFieldsQuery(viewType, libraryViewSettings), + includeItemTypes: itemType, + enableImageTypes: [ImageType.Thumb], + startIndex: libraryViewSettings.StartIndex + }, + { + signal: options?.signal + } + ); + break; + default: { + response = await getItemsApi(api).getItems( + { + userId: user.Id, + recursive: true, + imageTypeLimit: 1, + parentId: parentId ?? undefined, + enableImageTypes: [libraryViewSettings.ImageType, ImageType.Backdrop], + ...getFieldsQuery(viewType, libraryViewSettings), + ...getFiltersQuery(viewType, libraryViewSettings), + ...getLimitQuery(), + ...getAlphaPickerQuery(libraryViewSettings), + isFavorite: viewType === LibraryTab.Favorites ? true : undefined, + sortBy: [libraryViewSettings.SortBy], + sortOrder: [libraryViewSettings.SortOrder], + includeItemTypes: itemType, + startIndex: libraryViewSettings.StartIndex + }, + { + signal: options?.signal + } + ); + break; + } + } + return response.data; + } +}; + +export const useGetItemsViewByType = ( + viewType: LibraryTab, + parentId: ParentId, + itemType: BaseItemKind[], + libraryViewSettings: LibraryViewSettings +) => { + const currentApi = useApi(); + return useQuery({ + queryKey: [ + 'ItemsViewByType', + viewType, + parentId, + itemType, + libraryViewSettings + ], + queryFn: ({ signal }) => + fetchGetItemsViewByType( + currentApi, + viewType, + parentId, + itemType, + libraryViewSettings, + { signal } + ), + refetchOnWindowFocus: false, + keepPreviousData : true, + enabled: + [ + LibraryTab.Movies, + LibraryTab.Favorites, + LibraryTab.Collections, + LibraryTab.Trailers, + LibraryTab.Series, + LibraryTab.Episodes, + LibraryTab.Networks, + LibraryTab.Albums, + LibraryTab.AlbumArtists, + LibraryTab.Artists, + LibraryTab.Playlists, + LibraryTab.Songs, + LibraryTab.Books, + LibraryTab.Photos, + LibraryTab.Videos + ].includes(viewType) && !!parentId + }); +}; diff --git a/src/types/cardOptions.ts b/src/types/cardOptions.ts index 7864ab3157..6589782936 100644 --- a/src/types/cardOptions.ts +++ b/src/types/cardOptions.ts @@ -1,4 +1,5 @@ import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'; +import { CollectionType } from './collectionType'; export interface CardOptions { itemsContainer?: HTMLElement | null; @@ -32,7 +33,7 @@ export interface CardOptions { showUnplayedIndicator?: boolean; showChildCountIndicator?: boolean; lines?: number; - context?: string | null; + context?: CollectionType; action?: string | null; defaultShape?: string; indexBy?: string; diff --git a/src/types/interface.ts b/src/types/interface.ts deleted file mode 100644 index c577f84e2a..0000000000 --- a/src/types/interface.ts +++ /dev/null @@ -1,29 +0,0 @@ -export interface ViewQuerySettings { - showTitle?: boolean; - showYear?: boolean; - imageType?: string; - viewType?: string; - cardLayout?: boolean; - SortBy?: string | null; - SortOrder?: string | null; - IsPlayed?: boolean | null; - IsUnplayed?: boolean | null; - IsFavorite?: boolean | null; - IsResumable?: boolean | null; - Is4K?: boolean | null; - IsHD?: boolean | null; - IsSD?: boolean | null; - Is3D?: boolean | null; - VideoTypes?: string | null; - SeriesStatus?: string | null; - HasSubtitles?: boolean | null; - HasTrailer?: boolean | null; - HasSpecialFeature?: boolean | null; - ParentIndexNumber?: boolean | null; - HasThemeSong?: boolean | null; - HasThemeVideo?: boolean | null; - GenreIds?: string | null; - NameLessThan?: string | null; - NameStartsWith?: string | null; - StartIndex?: number; -} diff --git a/src/types/library.ts b/src/types/library.ts index 862ee3d6d3..37d0781cba 100644 --- a/src/types/library.ts +++ b/src/types/library.ts @@ -8,7 +8,7 @@ import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by'; export type ParentId = string | null | undefined; export interface LibraryViewProps { - parentId: string | null; + parentId: ParentId; } export enum FeatureFilters { diff --git a/src/utils/items.ts b/src/utils/items.ts index 08936a3fde..ce2efc8dc2 100644 --- a/src/utils/items.ts +++ b/src/utils/items.ts @@ -107,7 +107,7 @@ export const getFieldsQuery = ( export const getLimitQuery = () => { return { - limit: userSettings.libraryPageSize(undefined) || undefined + limit: userSettings.libraryPageSize(undefined) ?? undefined }; }; @@ -144,12 +144,12 @@ export const getSettingsKey = (viewType: LibraryTab, parentId: ParentId) => { return `${viewType} - ${parentId}`; }; -export const getDefaultLibraryViewSettings = (): LibraryViewSettings => { +export const getDefaultLibraryViewSettings = (viewType: LibraryTab): LibraryViewSettings => { return { ShowTitle: true, ShowYear: false, - ViewMode: ViewMode.GridView, - ImageType: ImageType.Primary, + ViewMode: viewType === LibraryTab.Songs ? ViewMode.ListView : ViewMode.GridView, + ImageType: viewType === LibraryTab.Networks ? ImageType.Thumb : ImageType.Primary, CardLayout: false, SortBy: ItemSortBy.SortName, SortOrder: SortOrder.Ascending, From 8bf3a64cd72cda69ac6da685ddeb117b62e3c3e1 Mon Sep 17 00:00:00 2001 From: Jason Beetham Date: Mon, 2 Oct 2023 22:42:05 -0600 Subject: [PATCH 20/31] Make the series and season Play button play the next episode --- src/components/playback/playbackmanager.js | 50 ++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index 05117fd08d..53b70ea445 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -1848,6 +1848,56 @@ class PlaybackManager { SortBy: options.shuffle ? 'Random' : 'SortName', MediaTypes: 'Audio' }); + } else if (firstItem.Type === 'Series' || firstItem.Type === 'Season') { + const apiClient = ServerConnections.getApiClient(firstItem.ServerId); + + promise = apiClient.getEpisodes(firstItem.SeriesId || firstItem.Id, { + IsVirtualUnaired: false, + IsMissing: false, + UserId: apiClient.getCurrentUserId(), + Fields: 'Chapters' + }).then(function (episodesResult) { + const originalResults = episodesResult.Items; + const isSeries = firstItem.Type === 'Series'; + + let foundItem = false; + + episodesResult.Items = episodesResult.Items.filter(function (e) { + if (foundItem) { + return true; + } + + if (!e.UserData.Played && (isSeries || e.SeasonId === firstItem.Id)) { + foundItem = true; + return true; + } + + return false; + }); + + if (episodesResult.Items.length === 0) { + if (isSeries) { + episodesResult.Items = originalResults; + } else { + episodesResult.Items = originalResults.filter(function (e) { + if (foundItem) { + return true; + } + + if (e.SeasonId === firstItem.Id) { + foundItem = true; + return true; + } + + return false; + }); + } + } + + episodesResult.TotalRecordCount = episodesResult.Items.length; + + return episodesResult; + }); } else if (firstItem.IsFolder && firstItem.CollectionType === 'homevideos') { promise = getItemsForPlayback(serverId, mergePlaybackQueries({ ParentId: firstItem.Id, From c470745176025fab44edd9d0dce09ce8565afa01 Mon Sep 17 00:00:00 2001 From: Jesse Date: Wed, 11 Oct 2023 08:43:55 +0000 Subject: [PATCH 21/31] Translated using Weblate (English (United Kingdom)) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/en_GB/ --- src/strings/en-gb.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/strings/en-gb.json b/src/strings/en-gb.json index 3725023495..3e19f5939b 100644 --- a/src/strings/en-gb.json +++ b/src/strings/en-gb.json @@ -1732,7 +1732,7 @@ "EnableAudioNormalizationHelp": "Audio normalisation will add a constant gain to keep the average at a desired level (-18dB).", "EnableAudioNormalization": "Audio Normalisation", "LabelEnableLUFSScan": "Enable LUFS scan", - "LabelEnableLUFSScanHelp": "Enable LUFS scan for music (This will take longer and more resources).", + "LabelEnableLUFSScanHelp": "Clients can normalise audio playback to get equal loudness across tracks. This will make library scans longer and take more resources.", "AllowCollectionManagement": "Allow this user to manage collections", "GetThePlugin": "Get the Plugin", "Notifications": "Notifications", @@ -1773,5 +1773,8 @@ "MachineTranslated": "Machine Translated", "ForeignPartsOnly": "Forced/Foreign parts only", "HearingImpairedShort": "HI/SDH", - "HeaderGuestCast": "Guest Stars" + "HeaderGuestCast": "Guest Stars", + "LabelIsHearingImpaired": "For hearing impaired (SDH)", + "BackdropScreensaver": "Backdrop Screensaver", + "LogoScreensaver": "Logo Screensaver" } From 145324b853edffe349ca9ef50e1c811d9cbd9a6e Mon Sep 17 00:00:00 2001 From: Mohammadreza Firoozeh Date: Wed, 11 Oct 2023 08:54:35 +0000 Subject: [PATCH 22/31] Translated using Weblate (Persian) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fa/ --- src/strings/fa.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/strings/fa.json b/src/strings/fa.json index e54ba0d234..1bf713557c 100644 --- a/src/strings/fa.json +++ b/src/strings/fa.json @@ -1602,5 +1602,8 @@ "IgnoreDtsHelp": "غیر فعال کردن این گزینه ممکن است برخی اشکالات را رفع کند، مثل نبودن صدا بر روی کانال هایی که جریان صدا و تصویر جداگانه دارند.", "LabelDummyChapterDurationHelp": "وقفه استخراج تصاویر فصل به ثانیه.", "HeaderDummyChapter": "تصاویر فصل", - "EnableAudioNormalization": "معمول سازی صوت" + "EnableAudioNormalization": "معمول سازی صوت", + "AllowCollectionManagement": "به این کاربر اجازه مدیریت مجموعه را بده", + "AllowSegmentDeletion": "تکه ها را پاک کن", + "AllowSegmentDeletionHelp": "پاک کردن تکه های قدیمی را بعد از فرستادن به کلاینت. این کار از ذخیره کل فایل transcode شده بر روی هارد جلوگیری می‌ کند. این تنها زمانی کار می کند که throttling فعال باشد. درصورت مشکل در هنگام پخش این ویژگی را غیرفعال کنید." } From 12c8fa3449cc6b8f86afd49ce3ef82716059b7e7 Mon Sep 17 00:00:00 2001 From: nextlooper42 Date: Wed, 11 Oct 2023 07:51:34 +0000 Subject: [PATCH 23/31] Translated using Weblate (Slovak) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sk/ --- src/strings/sk.json | 91 ++++++++++++++++++++++++++++++++------------- 1 file changed, 65 insertions(+), 26 deletions(-) diff --git a/src/strings/sk.json b/src/strings/sk.json index a4fea164af..33b31f3ce8 100644 --- a/src/strings/sk.json +++ b/src/strings/sk.json @@ -662,7 +662,7 @@ "TabLatest": "Najnovšie pridané", "TabMusic": "Hudba", "TabMyPlugins": "Moje zásuvné moduly", - "TabNetworks": "TV Siete", + "TabNetworks": "Stanice", "TabNfoSettings": "NFO nastavenia", "TabNotifications": "Upozornenia", "TabOther": "Ostatné", @@ -953,7 +953,7 @@ "GuideProviderSelectListings": "Výber zobrazenia", "GroupVersions": "Skupinové verzie", "FetchingData": "Načítavanie dodatočných dát", - "Extras": "Extras", + "Extras": "Bonusové materiály", "FastForward": "Rýchlo dopredu", "FFmpegSavePathNotFound": "Nepodarilo sa nám nájsť FFmpeg pomocou vami zadanej cesty. FFprobe je taktiež potrebná a musí existovať v rovnakom priečinku. Tieto komponenty sú za normálnych okolností zabalené spolu do toho istého priečinku. Prosím, overte zadanú cestu a skúste to znova.", "ErrorSavingTvProvider": "Nastala chyba pri ukladaní sprostredkovateľa TV vysielania. Prosím, uistite sa, že je prístupný a skúste to znova.", @@ -1081,11 +1081,11 @@ "MessagePluginInstallDisclaimer": "UPOZORNENIE: Inštalácia zásuvného modulu tretej strany má určité riziká. Modul môže obsahovať nestabilný alebo škodlivý kód a môže sa kedykoľvek zmeniť. Inštalujte zásuvné moduly len od autorov, ktorým dôverujete a majte na vedomí ich potenciálne následky, vrátane kontaktovania externých služieb, dlhšieho prehľadávanie knižníc alebo dodatočných procesov na pozadí.", "MessagePluginConfigurationRequiresLocalAccess": "Pre konfiguráciu tohoto zásuvného modulu sa prihláste priamo na lokálny server.", "MessagePlayAccessRestricted": "Prehrávanie tohoto obsahu je aktuálne obmedzené. Prosím, kontaktujte svojho administrátora servera pre viac informácií.", - "MessagePasswordResetForUsers": "Nasledujúci používatelia si nechali obnoviť heslo. Teraz sa môžu prihlásiť s PIN kódmi, ktoré použili k pri obnove hesla.", + "MessagePasswordResetForUsers": "Nasledujúcim používateľom boli resetované heslá. Teraz sa môžu prihlásiť pomocou kódov PIN, ktoré boli použité na resetovanie.", "MessageNoServersAvailable": "Žiadne servery neboli nájdené pomocou automatického objavovania serverov.", "MessageNoMovieSuggestionsAvailable": "V súčasnosti nie sú k dispozícií žiadne filmové návrhy. Začnite pozerať a hodnotiť vaše filmy, potom sa sem vráťte pre vaše odporúčania.", "MessageNoCollectionsAvailable": "Kolekcie vám umožnia užiť si vlastné zoskupenia filmov, seriálov a albumov. Kliknite na tlačítko '+' pre začatie vytvárania kolekcie.", - "MessageImageTypeNotSelected": "Prosím, vyberte typ obrázku z rozbaľovacieho menu.", + "MessageImageTypeNotSelected": "Z rozbaľovacej ponuky vyberte typ obrázka.", "MessageForgotPasswordInNetworkRequired": "Prosím, skúste to znova vo vašej domácej sieti pre zahájenie procesu obnovy hesla.", "MessageForgotPasswordFileCreated": "Nasledujúci súbor bol vytvorený na vašom serveri a obsahuje inštrukcie, ako postupovať", "MessageDownloadQueued": "Sťahovanie zaradené do fronty.", @@ -1095,7 +1095,7 @@ "MessageCreateAccountAt": "Vytvoriť účet v {0}", "MessageContactAdminToResetPassword": "Prosím, kontaktujte vášho systémového administrátora k obnoveniu hesla.", "MessageConfirmRevokeApiKey": "Ste si istý, že chcete odňať tento API kľúč? Aplikácie pripojené k tomuto serveru budú rázne ukončené.", - "Menu": "Menu", + "Menu": "Ponuka", "MediaIsBeingConverted": "Médium sa konvertuje do formátu, ktorý je kompatibilný so zariadením, kde sa médium prehráva.", "MediaInfoSampleRate": "Vzorkovacia frekvencia", "MediaInfoRefFrames": "Ref snímky", @@ -1397,7 +1397,7 @@ "SelectServer": "Vybrať server", "Restart": "Reštartovať", "ResetPassword": "Obnoviť heslo", - "QuickConnectAuthorizeSuccess": "Požiadavka autorizovaná", + "QuickConnectAuthorizeSuccess": "Úspešne ste overili vaše zariadenie!", "QuickConnectAuthorizeFail": "Neznámy kód pre Rýchle pripojenie", "QuickConnectAuthorizeCode": "Zadajte kód {0} pre prihlásenie", "QuickConnectActivationSuccessful": "Úspešne aktivované", @@ -1447,7 +1447,7 @@ "AspectRatioCover": "Obal", "VideoAudio": "Video Zvuk", "Video": "Video", - "AllowTonemappingHelp": "Tone-mapping can transform the dynamic range of a video from HDR to SDR while maintaining image details and colors, 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": "Mapovanie tónov umožňuje zmeniť dynamický rozsah videa z HDR na SDR bez straty dôležitých informácií pôvodného obrazu, ako sú detaily a farby. Táto funkcia v súčasnosti funguje len pre videá, ktoré obsahujú 10-bitové HDR10, HLG alebo Dolby Vision. Funkcia vyžaduje OpenCL alebo CUDA.", "LabelTonemappingThresholdHelp": "Parametre algoritmu mapovania tónov sú prispôsobené jednotlivým scénam. A tento prah sa používa na zistenie, či sa scéna zmenila alebo nie. Pokiaľ rozdiel medzi súčasnou priemernou svetlosťou snímku a priebežným priemerom tento prah prekročí, bude priemerná a vrchná svetlosť scény prepočítaná. Doporučené a predvolené hodnoty sú 0.8 a 0.2.", "LabelUDPPortRangeHelp": "Obmedzí UDP pripojenie Jellyfinu na tento rozsah. (Predvolená hodnota je 1024 - 65535).
Poznámka: Niektoré funkcie vyžadujú určité porty, ktoré sa môžu nachádzať mimo tohto rozsahu.", "Remuxing": "Remuxovanie", @@ -1508,7 +1508,7 @@ "DisablePlugin": "Zakázať", "EnablePlugin": "Povoliť", "Framerate": "Snímková frekvencia", - "DirectPlayHelp": "Zdrojový súbor je s klientom plne kompatibilný a relácia ho preto prijíma bez dodatočných modifikácií.", + "DirectPlayHelp": "Zdrojový súbor je úplne kompatibilný s týmto klientom a relácia prijíma súbor bez úprav.", "HeaderContinueReading": "Pokračovať v čítaní", "LabelSyncPlaySettingsDescription": "Zmeniť nastavenia SyncPlay", "LabelSlowResponseTime": "Čas v milisekundách, ktorý je považovaný za pomalú odozvu", @@ -1679,7 +1679,7 @@ "OptionDateShowAdded": "Dátum pridania seriálu", "OptionDateEpisodeAdded": "Dátum pridania epizódy", "IgnoreDtsHelp": "Vypnutím sa môžu vyriešiť niektoré problémy, napr. chýbajúci zvuk pri kanáloch so samostatnými zvukovými a video streamami.", - "IgnoreDts": "Ignorovať DTS (dekódovacia časová pečiatka)", + "IgnoreDts": "Ignorovať DTS (dekódovacia časová značka)", "Unreleased": "Zatiaľ nevydané", "EnableCardLayout": "Zobraziť vizuálny CardBox", "MessageNoItemsAvailable": "Momentálne nie sú k dispozícii žiadne položky.", @@ -1694,11 +1694,11 @@ "LabelDummyChapterCount": "Limit", "LabelDummyChapterCountHelp": "Maximálny počet obrázkov kapitol, ktoré budú extrahované pre každý mediálny súbor.", "LabelChapterImageResolution": "Rozlíšenie", - "LabelChapterImageResolutionHelp": "Rozlíšenie extrahovaných obrázkov kapitol. Zmena tohto nastavenia nemá žiaden vplyv na existujúce kapitoly.", + "LabelChapterImageResolutionHelp": "Rozlíšenie extrahovaných obrázkov kapitol. Zmena tohto nastavenia nebude mať žiadny vplyv na existujúce fiktívne kapitoly.", "ResolutionMatchSource": "Rovnaké ako zdroj", "SaveRecordingNFOHelp": "Uloží metadáta z EPG položiek sprievodcu spolu s médiami.", "SaveRecordingImages": "Uložiť obrázky EPG nahrávky", - "LabelDummyChapterDurationHelp": "Interval medzi kapitolami. Vytváranie kapitol je možné vypnúť nastavením na 0. Zmena tohto nastavenia nemá žiaden vplyv na existujúce kapitoly.", + "LabelDummyChapterDurationHelp": "Interval medzi fiktívnymi kapitolami. Nastavením na 0 vypnete generovanie fiktívnych kapitol. Zmena tejto hodnoty nebude mať žiadny vplyv na existujúce fiktívne kapitoly.", "SaveRecordingNFO": "Uložiť metadáta nahrávky zo sprievodcu EPG do NFO", "SaveRecordingImagesHelp": "Uloží obrázky z EPG položiek sprievodcu spolu s médiami.", "HeaderRecordingMetadataSaving": "Metadáta nahrávok", @@ -1706,36 +1706,75 @@ "LabelEnableAudioVbr": "Povoliť kódovanie zvuku VBR", "HeaderPerformance": "Výkon", "AllowCollectionManagement": "Povoliť tomuto používateľovi spravovať kolekcie", - "LabelParallelImageEncodingLimitHelp": "Maximálny počet kódovania obrázkov, ktoré môžu naraz bežať. Nastavením na 0 bude limit nastavení podľa parametrov systému.", - "TonemappingModeHelp": "Vyberte režim mapovania tónu. Ak narazíte na preexponované svetlé miesta, skúste prepnúť na režim RGB.", + "LabelParallelImageEncodingLimitHelp": "Maximálny počet kódovaní obrazu, ktoré môžu bežať paralelne. Nastavením tejto hodnoty na 0 sa zvolí limit na základe špecifikácie vášho systému.", + "TonemappingModeHelp": "Vyberte režim mapovania tónov. Ak sa vyskytnú preexponované svetlé miesta, skúste prepnúť na režim RGB.", "Featurette": "Stredne dlhý film", "Short": "Krátky film", - "PasswordRequiredForAdmin": "Administrátorské úcty musia mať nastavené heslo.", - "LabelTonemappingMode": "Režim mapovania tónu", - "SubtitleCyan": "Tyrkysová", - "SubtitleGray": "Sivá", + "PasswordRequiredForAdmin": "Pre administrátorské účty sa vyžaduje heslo.", + "LabelTonemappingMode": "Režim mapovania tónov", + "SubtitleCyan": "Azúrová", + "SubtitleGray": "Šedá", "Studio": "Štúdio", "SubtitleBlue": "Modrá", "SubtitleBlack": "Čierna", "SubtitleGreen": "Zelená", - "SubtitleLightGray": "Svetlo sivá", + "SubtitleLightGray": "Svetlo šedá", "SubtitleRed": "Červená", "SubtitleYellow": "Žltá", "SubtitleWhite": "Biela", "Select": "Vybrať", - "EnableAudioNormalization": "Normalizácia hlasitosti", + "EnableAudioNormalization": "Normalizácia zvuku", "GetThePlugin": "Získať zásuvný modul", "LabelParallelImageEncodingLimit": "Limit paralelného kódovania obrázkov", "Notifications": "Upozornenia", "NotificationsMovedMessage": "Funkcia upozornení bola presunutá do zásuvného modulu Webhook.", - "PreferEmbeddedExtrasTitlesOverFileNames": "Preferovať vložené názvy pred názvami súborov pre doplnky", - "PreferEmbeddedExtrasTitlesOverFileNamesHelp": "Doplnky väčšinou majú totožní vložení názov ako nadriadená položka. Zaškrtnutím ich môžete napriek uprednostniť.", - "SubtitleMagenta": "Fialová", - "LabelEnableAudioVbrHelp": "Premenlivý bitový tok ponúka lepší pomer medzi kvalitou a priemerným bitovým tokom, ale niekdy môže spôsobiť dodatočné načítavanie alebo problémy s kompatibilitou.", + "PreferEmbeddedExtrasTitlesOverFileNames": "Preferovať vložené názvy pred názvami súborov pre bonusové materiály", + "PreferEmbeddedExtrasTitlesOverFileNamesHelp": "Bonusové materiály majú často rovnaký vložený názov ako nadradená položka, zaškrtnite túto možnosť, aby ste aj tak použili vložené názvy.", + "SubtitleMagenta": "Magenta", + "LabelEnableAudioVbrHelp": "Variabilný dátový tok ponúka lepší pomer kvality a priemerného dátového toku, ale v niektorých zriedkavých prípadoch môže spôsobiť problémy s vyrovnávacou pamäťou a kompatibilitou.", "MenuClose": "Zatvoriť ponuku", - "UserMenu": "Užívateľská ponuka", + "UserMenu": "Použí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).", + "LabelEnableLUFSScanHelp": "Klienti môžu normalizovať prehrávanie zvuku, aby sa dosiahla rovnaká hlasitosť všetkých stôp. (Predlžuje prehľadávanie knižnice a je náročnejšie na výkon).", "MenuOpen": "Otvoriť ponuku", - "AllowSegmentDeletion": "Zmazať oddiel" + "AllowSegmentDeletion": "Zmazať segmenty", + "LogoScreensaver": "Šetrič obrazovky s logom", + "AllowAv1Encoding": "Povoliť kódovanie do formátu AV1", + "BackdropScreensaver": "Šetrič obrazovky s pozadím", + "PleaseConfirmRepositoryInstallation": "Kliknutím na tlačidlo OK potvrďte, že ste si prečítali vyššie uvedené informácie a chcete pokračovať v inštalácii repozitára zásuvných modulov.", + "UnknownError": "Došlo k neznámej chybe.", + "LabelIsHearingImpaired": "Titulky pre nepočujúcich", + "LabelSyncPlayNoGroups": "Nie sú k dispozícii žiadne skupiny", + "HeaderGuestCast": "Hosťujúce hviezdy", + "LabelDate": "Dátum", + "LabelLevel": "Úroveň", + "MessageRepositoryInstallDisclaimer": "UPOZORNENIE: Inštalácia repozitárov zásuvných modulov tretích strán so sebou prináša určité riziká. Môžu obsahovať nestabilný alebo škodlivý kód a môžu sa kedykoľvek zmeniť. Inštalujte len repozitáre od autorov, ktorým dôverujete.", + "HeaderEpisodesStatus": "Stav epizód", + "LabelSystem": "Systém", + "LogLevel.Trace": "Stopa", + "LogLevel.Debug": "Debug", + "GoHome": "Prejsť na domovskú obrazovku", + "LabelBackdropScreensaverInterval": "Interval šetriča obrazovky s pozadím", + "LabelBackdropScreensaverIntervalHelp": "Čas v sekundách medzi rôznymi pozadiami pri použití šetriča obrazovky s pozadím.", + "LogLevel.Information": "Informácia", + "LabelDeveloper": "Vývojár", + "LabelMediaDetails": "Podrobnosti o médiách", + "GridView": "Zobrazenie v mriežke", + "ListView": "Zobrazenie v zozname", + "LogLevel.Warning": "Upozornenie", + "LogLevel.Error": "Chyba", + "LogLevel.Critical": "Kritická", + "LogLevel.None": "Žiadny", + "EnableAudioNormalizationHelp": "Normalizácia zvuku pridá konštantné zosilnenie, aby sa priemer hlasitosti udržal na požadovanej úrovni (-18 dB).", + "HeaderConfirmRepositoryInstallation": "Potvrdiť inštaláciu repozitára zásuvných modulov", + "Unknown": "Neznámy", + "AiTranslated": "Preložené pomocou AI", + "MachineTranslated": "Strojovo preložené", + "ForeignPartsOnly": "Iba vynútené", + "HearingImpairedShort": "Titulky pre nepočujúcich", + "LabelThrottleDelaySeconds": "Obmedziť po", + "LabelSegmentKeepSeconds": "Doba ponechania segmentov", + "LabelThrottleDelaySecondsHelp": "Čas v sekundách, po ktorom bude prekódovanie obmedzené. Musí byť dostatočne veľký, aby mal klient v rezerve dostatočné množstvo prehrávaného súboru. Funguje len vtedy, ak je povolená funkcia Obmedziť prekódovanie.", + "AllowSegmentDeletionHelp": "Odstránenie starých segmentov po ich odoslaní klientovi. Tým sa zabráni tomu, aby sa celý prekódovaný súbor musel ukladať na disk. Funguje len so zapnutou funkciou Obmedziť prekódovanie. Ak sa vyskytnú problémy s prehrávaním, vypnite túto funkciu.", + "LabelSegmentKeepSecondsHelp": "Čas v sekundách, počas ktorého budú segmenty uložené. Musí byť dlhší ako je čas určený v \"Obmedziť po\". Funguje len vtedy, ak je povolená funkcia Zmazania segmentov." } From 2536ddf8be92b2b3b19235512e06fb3a35621d43 Mon Sep 17 00:00:00 2001 From: arminius-smh Date: Wed, 11 Oct 2023 16:30:30 +0200 Subject: [PATCH 24/31] Remove more package caches from docker images --- deployment/Dockerfile.centos | 3 ++- deployment/Dockerfile.debian | 2 +- deployment/Dockerfile.fedora | 3 ++- deployment/Dockerfile.portable | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/deployment/Dockerfile.centos b/deployment/Dockerfile.centos index 39a8b1fbe3..146956fe80 100644 --- a/deployment/Dockerfile.centos +++ b/deployment/Dockerfile.centos @@ -15,7 +15,8 @@ RUN yum update -y \ && yum install -y rpmdevtools git autoconf automake glibc-devel gcc-c++ make \ && 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 \ - && yum clean all + && yum clean all \ + && rm -rf /var/cache/dnf # 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 68c62cf792..76a907e48d 100644 --- a/deployment/Dockerfile.debian +++ b/deployment/Dockerfile.debian @@ -18,7 +18,7 @@ RUN apt-get update \ && 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 \ - && rm -rf /var/lib/apt/lists/* + && rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/* # Link to build script RUN ln -sf ${SOURCE_DIR}/deployment/build.debian /build.sh diff --git a/deployment/Dockerfile.fedora b/deployment/Dockerfile.fedora index efcd602ce9..499377a2dc 100644 --- a/deployment/Dockerfile.fedora +++ b/deployment/Dockerfile.fedora @@ -13,7 +13,8 @@ ENV IS_DOCKER=YES RUN dnf update -y \ && dnf 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 \ - && dnf clean all + && dnf clean all \ + && rm -rf /var/cache/dnf # 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 4bdbe80a72..c06dfcd151 100644 --- a/deployment/Dockerfile.portable +++ b/deployment/Dockerfile.portable @@ -17,7 +17,7 @@ RUN apt-get update \ && 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 \ - && rm -rf /var/lib/apt/lists/* + && rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/* # Link to build script RUN ln -sf ${SOURCE_DIR}/deployment/build.portable /build.sh From 8725edb9e412b667fe25a992583fd1c7444200ac Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Wed, 11 Oct 2023 20:00:29 +0300 Subject: [PATCH 25/31] use logical OR ( || ) --- src/utils/items.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/items.ts b/src/utils/items.ts index ce2efc8dc2..b80c8ed057 100644 --- a/src/utils/items.ts +++ b/src/utils/items.ts @@ -107,7 +107,7 @@ export const getFieldsQuery = ( export const getLimitQuery = () => { return { - limit: userSettings.libraryPageSize(undefined) ?? undefined + limit: userSettings.libraryPageSize(undefined) || undefined }; }; From 0a7ec2e27032c7bfff5a35f61bdaba5d0dd7885b Mon Sep 17 00:00:00 2001 From: Nyanmisaka <799610810@qq.com> Date: Thu, 12 Oct 2023 06:35:53 +0000 Subject: [PATCH 26/31] Translated using Weblate (Chinese (Simplified)) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hans/ --- src/strings/zh-cn.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/strings/zh-cn.json b/src/strings/zh-cn.json index 47986abc89..92a998c75c 100644 --- a/src/strings/zh-cn.json +++ b/src/strings/zh-cn.json @@ -1353,7 +1353,7 @@ "LabelOpenclDevice": "OpenCL 设备", "LabelOpenclDeviceHelp": "这是用于色调映射的 OpenCL 设备。 点左边是平台号,右边是平台上的设备号。 默认值为 0.0。 需要支持OpenCL 硬件加速的 FFmpeg 应用程序。", "EnableTonemapping": "启用色调映射", - "AllowTonemappingHelp": "色调映射可以将视频的动态范围从HDR转换为SDR,同时保持图像细节和颜色,这些都是表示原始场景的非常重要的信息。目前只适用于10bit HDR10、HLG和DoVi视频。这需要相应的OpenCL或CUDA运行库。", + "AllowTonemappingHelp": "色调映射可以将视频的动态范围从 HDR 变换成 SDR,同时保持图像细节与颜色等对于表现原始场景非常重要的信息。目前仅对 10bit HDR10,HLG 和 DoVi 视频生效。此项需要对应的 OpenCL 或 CUDA 运行库。", "LabelTonemappingAlgorithm": "选择要使用的色调映射算法", "TonemappingAlgorithmHelp": "色调映射可以微调。如果你不是很熟悉这些选项,保持默认即可。建议值为 'BT.2390'。", "LabelTonemappingRange": "色调映射范围", @@ -1764,7 +1764,7 @@ "HeaderEpisodesStatus": "剧集状态", "LabelBackdropScreensaverInterval": "屏幕保护程序间隔", "LabelBackdropScreensaverIntervalHelp": "不同屏幕保护切换的时间间隔秒数。", - "AllowAv1Encoding": "允许以AV1格式进行编码", + "AllowAv1Encoding": "允许以 AV1 格式进行编码", "HeaderGuestCast": "特邀嘉宾", "GridView": "网格视图", "ListView": "列表视图", From 71e431d562b481f3c8868e6d96f5d8c9d0755d2f Mon Sep 17 00:00:00 2001 From: Sergiu B Date: Thu, 12 Oct 2023 13:51:40 +0000 Subject: [PATCH 27/31] Translated using Weblate (Romanian) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ro/ --- src/strings/ro.json | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/strings/ro.json b/src/strings/ro.json index ecd87b94b6..b7b658d5a4 100644 --- a/src/strings/ro.json +++ b/src/strings/ro.json @@ -1698,5 +1698,36 @@ "SubtitleGreen": "Verde", "SubtitleWhite": "Alb", "SubtitleYellow": "Galben", - "VideoRangeTypeNotSupported": "Tipul intervalului video nu este suportat" + "VideoRangeTypeNotSupported": "Tipul intervalului video nu este suportat", + "Unreleased": "Indisponibil momentan", + "LabelIsHearingImpaired": "Pentru deficiente de auz (SDH)", + "PasswordRequiredForAdmin": "O parola este necesara pentru conturile de administrator.", + "Studio": "Studio", + "LabelSyncPlayNoGroups": "Niciun grup disponibil", + "Notifications": "Notificari", + "LabelDate": "Data", + "LabelLevel": "Nivel", + "LabelSystem": "Sistem", + "LabelMediaDetails": "Detalii media", + "LogLevel.Information": "Informatie", + "Unknown": "Necunoscut", + "GoHome": "Catre Ecranul Principal", + "UnknownError": "A aparut o eroare neprevazuta.", + "LogLevel.Warning": "Avertisment", + "LogLevel.Error": "Eroare", + "LogLevel.Critical": "Critic", + "LabelDeveloper": "Dezvoltator", + "AllowCollectionManagement": "Permite acestui utilizator sa administreze colectiile", + "ListView": "Vizualizare ca lista", + "MenuOpen": "Deschideti Meniul", + "MenuClose": "Inchidere Meniu", + "UserMenu": "Meniu Utilizator", + "Select": "Selecteaza", + "EnableAudioNormalizationHelp": "Normalizarea audio va adauga o crestere constanta pentru a mentine media la un nivel dorit (-18 decibeli).", + "AiTranslated": "Tradus de catre IA", + "AllowSegmentDeletion": "Sterge segmentele", + "AllowSegmentDeletionHelp": "Sterge segmentele vechi dupa ce acestea au fost trimise catre client. Previne necesitatea de a stoca intregul fisier transcodificat pe disc. Va functiona doar cu throttling activat. Opriti aceasta optiune daca intampinati probleme de redare.", + "LabelThrottleDelaySeconds": "Limitare dupa", + "LabelSegmentKeepSeconds": "Timpul pentru a pastra segmentele", + "EnableAudioNormalization": "Normalizare audio" } From c25a1589263a3dfbfbf37c308a53bfecfdef85b7 Mon Sep 17 00:00:00 2001 From: Ignio-dev Date: Fri, 13 Oct 2023 07:16:40 +0000 Subject: [PATCH 28/31] Translated using Weblate (Hebrew) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/he/ --- src/strings/he.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/strings/he.json b/src/strings/he.json index 089bbe949b..3f8f52377e 100644 --- a/src/strings/he.json +++ b/src/strings/he.json @@ -1202,5 +1202,13 @@ "LabelKodiMetadataEnableExtraThumbsHelp": "תמונות שהורדו יכולות להישמר לשדות extrafanart ו-extrathumbs בו זמנית לצורך התאמה מירבית לסקינים של קודי.", "LabelKodiMetadataEnableExtraThumbs": "העתק extrafanart לשדה extrathumbs", "LabelGroupMoviesIntoCollectionsHelp": "כל הסרטים באוסף יופיעו בתור פריט מקובץ אחד ברשימות סרטים.", - "LabelHomeScreenSectionValue": "איזור {0} בעמוד הבית" + "LabelHomeScreenSectionValue": "איזור {0} בעמוד הבית", + "LabelExtractChaptersDuringLibraryScan": "חלץ את תמונות הפרק תוך כדי סריקת הספריה", + "LabelBackdropScreensaverInterval": "interval שומר המסך של הרקע", + "LabelEnableLUFSScan": "הפעלת סריקת LUFS", + "LabelEnableAutomaticPortMapHelp": "בצע port forward בצורה אוטומטית מפורטים בראוטר אל פורטים לוקאליים על השרת באמצעות UPnP. ייתכן שזה לא יעבוד עם דגמי נתב מסוימים או עם קונפיגורציות רשת מסוימות. שינויים לא יחולו עד לאחר הפעלה מחדש של השרת.", + "BackdropScreensaver": "שומר המסך של הרקע", + "LabelExtractChaptersDuringLibraryScanHelp": "צור תמונות פרקים כאשר סרטונים מיובאים תוך כדי סריקת הספריה. אחרת, הם יחולצו תוך כדי המשימה המתוזמנת העוסקת בתמונות הפרק, מה שיאפשר לסריקת הספריה הרגילה להסתיים מהר יותר.", + "LabelThrottleDelaySecondsHelp": "כמות הזמן בשניות שלאחריה המקודד יגיע ל-throttle. חייב להיות גדול מספיק כדי שהלקוח ישמור על buffer גדול מספיק. עובד רק אם throttling מופעל.", + "HeaderGuestCast": "כוכבים אורחים" } From 216e01aa55f49eae9c290e2da961f801cca74e15 Mon Sep 17 00:00:00 2001 From: pgk83 Date: Fri, 13 Oct 2023 14:30:17 +0000 Subject: [PATCH 29/31] Translated using Weblate (Hungarian) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/hu/ --- src/strings/hu.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/strings/hu.json b/src/strings/hu.json index 506c533c49..7d3ca7e3c7 100644 --- a/src/strings/hu.json +++ b/src/strings/hu.json @@ -1627,7 +1627,7 @@ "AllowEmbeddedSubtitlesAllowImageOption": "Kép engedélyezése", "AllowEmbeddedSubtitlesAllowNoneOption": "Összes tiltása", "AllowEmbeddedSubtitlesAllowAllOption": "Összes engedélyezése", - "AllowEmbeddedSubtitlesHelp": "Tiltsa le a médiatárolókba csomagolt feliratokat. Teljes könyvtárfrissítést igényel.", + "AllowEmbeddedSubtitlesHelp": "Letiltja a médiatárolókba csomagolt feliratokat. Teljes könyvtárfrissítést igényel.", "AllowEmbeddedSubtitles": "Különféle típusú beágyazott feliratok letiltása", "ShowParentImages": "Sorozatképek megjelenítése", "NextUpRewatching": "Újranézés", @@ -1767,5 +1767,6 @@ "AllowAv1Encoding": "AV1 kódolás engedélyezése", "GridView": "Rács Nézet", "ListView": "Lista Nézet", - "MachineTranslated": "Gépi fordítás" + "MachineTranslated": "Gépi fordítás", + "AiTranslated": "Gépi fordítás" } From ae48e2a00737fd5e1f21b2e64f92e56ffd530427 Mon Sep 17 00:00:00 2001 From: Oskari Lavinto Date: Sat, 14 Oct 2023 15:31:13 +0000 Subject: [PATCH 30/31] Translated using Weblate (Finnish) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fi/ --- src/strings/fi.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/strings/fi.json b/src/strings/fi.json index 4ecc915066..a2ba7d3d45 100644 --- a/src/strings/fi.json +++ b/src/strings/fi.json @@ -1437,7 +1437,7 @@ "QuickConnectNotActive": "Pikayhdistys ei ole tällä palvelimella käytössä", "QuickConnectNotAvailable": "Pyydä palvelimen ylläpitoa ottamaan Pikayhdistys käyttöön", "QuickConnectInvalidCode": "Virheellinen Pikayhdistyskoodi", - "QuickConnectDescription": "Kirjautuaksesi Pikayhdistyksellä, valitse 'Pikayhdistys'-painike laitteelta, josta yrität kirjautua ja syötä alla oleva koodi.", + "QuickConnectDescription": "Kirjautuaksesi Pikayhdistyksellä, valitse 'Pikayhdistys' laitteelta, josta yrität kirjautua ja syötä näytettävä koodi alle.", "QuickConnectDeactivated": "Pikayhdistys katkaistiin ennen kirjautumispyynnön hyväksyntää", "QuickConnectAuthorizeSuccess": "Laitteesi on todennettu!", "QuickConnectAuthorizeFail": "Tuntematon Pikayhdistyskoodi", From 1f358cf80ee553200375572e01b1ad33475c77d4 Mon Sep 17 00:00:00 2001 From: felix920506 Date: Mon, 9 Oct 2023 13:59:26 -0400 Subject: [PATCH 31/31] Change fallback fonts help text to link documentation --- src/strings/en-us.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 46ac586951..1ba80e7dfc 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -1624,7 +1624,7 @@ "HeaderSelectFallbackFontPath": "Select Fallback Font Folder Path", "HeaderSelectFallbackFontPathHelp": "Browse or enter the path of the fallback font folder to use for rendering ASS/SSA subtitles.", "LabelFallbackFontPath": "Fallback font folder path", - "LabelFallbackFontPathHelp": "Specify a path containing fallback fonts for rendering ASS/SSA subtitles. The maximum allowed total font size is 20 MB. Lightweight and web-friendly font formats such as woff2 are recommended.", + "LabelFallbackFontPathHelp": "Specify a path containing fallback fonts for rendering subtitles for some clients. The maximum allowed total font size is 20 MB. Please refer to the Jellyfin documentation on fonts for more info.", "EnableFallbackFont": "Enable fallback fonts", "EnableFallbackFontHelp": "Enable custom alternate fonts. This can avoid the problem of incorrect subtitle rendering.", "AspectRatioCover": "Cover",