mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-05 13:38:27 +01:00
Compare commits
29 Commits
cleanup/co
...
feature/sy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab42e8a576 | ||
|
|
0e93cd5385 | ||
|
|
96b4121c1f | ||
|
|
f7033e7abb | ||
|
|
0d796d01b8 | ||
|
|
46d96d5965 | ||
|
|
7d16e7d5c7 | ||
|
|
ceb9b5a1ae | ||
|
|
1144ff5049 | ||
|
|
4d508a4315 | ||
|
|
915a4febbb | ||
|
|
88163eb531 | ||
|
|
46bd2a784e | ||
|
|
0a36fdfbec | ||
|
|
45d1f752d6 | ||
|
|
54ee507209 | ||
|
|
338fb9713b | ||
|
|
939fd2512d | ||
|
|
32c99de874 | ||
|
|
c232e433bf | ||
|
|
07e2faff07 | ||
|
|
8507699cdd | ||
|
|
21fb056586 | ||
|
|
1d79b513f3 | ||
|
|
863dffd944 | ||
|
|
6aa0868bfd | ||
|
|
27c400a54a | ||
|
|
261f7cc0cd | ||
|
|
d06daef933 |
@@ -45,4 +45,4 @@ This file previously contained facts about the codebase learned from past sessio
|
|||||||
|
|
||||||
- **Streamystats components location**: Streamystats TV components are at `components/home/StreamystatsRecommendations.tv.tsx` and `components/home/StreamystatsPromotedWatchlists.tv.tsx`. The watchlist detail page (which shows items in a grid) is at `app/(auth)/(tabs)/(watchlists)/[watchlistId].tsx`. _(2026-01-25)_
|
- **Streamystats components location**: Streamystats TV components are at `components/home/StreamystatsRecommendations.tv.tsx` and `components/home/StreamystatsPromotedWatchlists.tv.tsx`. The watchlist detail page (which shows items in a grid) is at `app/(auth)/(tabs)/(watchlists)/[watchlistId].tsx`. _(2026-01-25)_
|
||||||
|
|
||||||
- **Platform-specific file suffix (.tv.tsx) does NOT work**: The `.tv.tsx` file suffix does NOT work for either pages or components in this project. Metro bundler doesn't resolve platform-specific suffixes. Instead, use `Platform.isTV` conditional rendering within a single file. For pages: check `Platform.isTV` at the top and return the TV component early. For components: create separate `MyComponent.tsx` and `TVMyComponent.tsx` files and use `Platform.isTV` to choose which to render. _(2026-01-26)_
|
- **Platform-specific file suffix (.tv.tsx) does NOT work**: The `.tv.tsx` file suffix does NOT work for either pages or components in this project. Metro bundler doesn't resolve platform-specific suffixes. Instead, use `Platform.isTV` conditional rendering within a single file. For pages: check `Platform.isTV` at the top and return the TV component early. For components: create separate `MyComponent.tsx` and `TVMyComponent.tsx` files and use `Platform.isTV` to choose which to render. _(2026-01-26)_
|
||||||
|
|||||||
25
.eas/build/android-production-apk.yml
Normal file
25
.eas/build/android-production-apk.yml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Custom EAS Build config for Android phone APK (downloadable artifact).
|
||||||
|
# Same bun-forcing flow as android-production.yml, but builds an APK
|
||||||
|
# (assembleRelease) instead of an AAB — for sideloading / GitHub artifact.
|
||||||
|
# Referenced from eas.json: build.production-apk.android.config
|
||||||
|
build:
|
||||||
|
name: Android phone APK (bun)
|
||||||
|
steps:
|
||||||
|
- eas/checkout
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Install dependencies (bun, frozen)
|
||||||
|
command: bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Prebuild (Android, bun)
|
||||||
|
command: bunx expo prebuild --platform android --no-install
|
||||||
|
|
||||||
|
- eas/configure_android_version
|
||||||
|
- eas/inject_android_credentials
|
||||||
|
|
||||||
|
- eas/run_gradle:
|
||||||
|
inputs:
|
||||||
|
command: :app:assembleRelease
|
||||||
|
|
||||||
|
- eas/find_and_upload_build_artifacts
|
||||||
27
.eas/build/android-production-tv.yml
Normal file
27
.eas/build/android-production-tv.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Custom EAS Build config for Android TV APK (downloadable artifact).
|
||||||
|
# Same bun-forcing flow, with EXPO_TV=1 (set via the profile env in
|
||||||
|
# eas.json) so prebuild generates the TV variant. Builds an APK for
|
||||||
|
# sideloading onto Android TV devices.
|
||||||
|
# Referenced from eas.json: build.production-apk-tv.android.config
|
||||||
|
build:
|
||||||
|
name: Android TV APK (bun)
|
||||||
|
steps:
|
||||||
|
- eas/checkout
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Install dependencies (bun, frozen)
|
||||||
|
command: bun install --frozen-lockfile
|
||||||
|
|
||||||
|
# EXPO_TV=1 comes from the profile env, so prebuild targets Android TV.
|
||||||
|
- run:
|
||||||
|
name: Prebuild (Android TV, bun)
|
||||||
|
command: bunx expo prebuild --platform android --no-install
|
||||||
|
|
||||||
|
- eas/configure_android_version
|
||||||
|
- eas/inject_android_credentials
|
||||||
|
|
||||||
|
- eas/run_gradle:
|
||||||
|
inputs:
|
||||||
|
command: :app:assembleRelease
|
||||||
|
|
||||||
|
- eas/find_and_upload_build_artifacts
|
||||||
38
.eas/build/android-production.yml
Normal file
38
.eas/build/android-production.yml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Custom EAS Build config for Android (production AAB).
|
||||||
|
#
|
||||||
|
# Why this exists: EAS's managed build can't detect Bun's text lockfile
|
||||||
|
# (bun.lock) and falls back to yarn, which breaks our install. The managed
|
||||||
|
# steps `eas/install_node_modules` and `eas/prebuild` both use "the package
|
||||||
|
# manager detected based on your project", so we replace them with explicit
|
||||||
|
# `bun` commands. Everything else uses EAS's built-in functions so we still
|
||||||
|
# get remote versioning, credentials, and artifact upload.
|
||||||
|
#
|
||||||
|
# Referenced from eas.json: build.production.android.config = android-production.yml
|
||||||
|
build:
|
||||||
|
name: Android production (bun)
|
||||||
|
steps:
|
||||||
|
- eas/checkout
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Install dependencies (bun, frozen)
|
||||||
|
command: bun install --frozen-lockfile
|
||||||
|
|
||||||
|
# android/ is gitignored, so generate native code fresh. --no-install
|
||||||
|
# because deps are already installed above; bunx keeps it on bun.
|
||||||
|
- run:
|
||||||
|
name: Prebuild (Android, bun)
|
||||||
|
command: bunx expo prebuild --platform android --no-install
|
||||||
|
|
||||||
|
# Applies the EAS-resolved remote versionCode/versionName (autoIncrement
|
||||||
|
# in eas.json) into the freshly prebuilt android/ project.
|
||||||
|
- eas/configure_android_version
|
||||||
|
|
||||||
|
# Injects the remote Android keystore / signing config.
|
||||||
|
- eas/inject_android_credentials
|
||||||
|
|
||||||
|
# Build the Play Store app bundle (.aab).
|
||||||
|
- eas/run_gradle:
|
||||||
|
inputs:
|
||||||
|
command: :app:bundleRelease
|
||||||
|
|
||||||
|
- eas/find_and_upload_build_artifacts
|
||||||
44
.eas/build/ios-production.yml
Normal file
44
.eas/build/ios-production.yml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Custom EAS Build config for iOS + tvOS (App Store), forcing bun.
|
||||||
|
#
|
||||||
|
# Shared by both the iPhone profile (production) and the tvOS profile
|
||||||
|
# (production_tv). The profile decides the rest:
|
||||||
|
# - production_tv sets EXPO_TV=1 (env) so prebuild targets tvOS, and
|
||||||
|
# credentialsSource: local (EAS can't manage tvOS creds remotely).
|
||||||
|
# - production uses remote-managed iOS credentials.
|
||||||
|
#
|
||||||
|
# Like the Android configs, this replaces eas/install_node_modules and
|
||||||
|
# eas/prebuild (both auto-detect the wrong package manager) with explicit
|
||||||
|
# bun commands, and keeps EAS built-ins for credentials/version/fastlane.
|
||||||
|
build:
|
||||||
|
name: iOS/tvOS App Store (bun)
|
||||||
|
steps:
|
||||||
|
- eas/checkout
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Install dependencies (bun, frozen)
|
||||||
|
command: bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- eas/resolve_apple_team_id_from_credentials:
|
||||||
|
id: resolve_team
|
||||||
|
|
||||||
|
# android/ + ios/ are gitignored, so generate native code fresh.
|
||||||
|
# EXPO_TV (from the profile env) selects iPhone vs tvOS. --no-install
|
||||||
|
# skips JS + pod install; we install pods explicitly below with bun deps.
|
||||||
|
- run:
|
||||||
|
name: Prebuild (iOS/tvOS, bun)
|
||||||
|
command: bunx expo prebuild --platform ios --no-install
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Install CocoaPods
|
||||||
|
working_directory: ./ios
|
||||||
|
command: pod install
|
||||||
|
|
||||||
|
- eas/configure_ios_credentials
|
||||||
|
- eas/configure_ios_version
|
||||||
|
|
||||||
|
- eas/generate_gymfile_from_template:
|
||||||
|
inputs:
|
||||||
|
credentials: ${ eas.job.secrets.buildCredentials }
|
||||||
|
|
||||||
|
- eas/run_fastlane
|
||||||
|
- eas/find_and_upload_build_artifacts
|
||||||
134
.github/workflows/release.yml
vendored
134
.github/workflows/release.yml
vendored
@@ -1,9 +1,14 @@
|
|||||||
name: 🚀 Release (EAS Build + Submit)
|
name: 🚀 Release (EAS build + submit)
|
||||||
|
|
||||||
# Cloud EAS build + auto-submit for iOS, tvOS and Android on merge to main.
|
# On merge to main (gated by the `production` GitHub Environment approval),
|
||||||
# A manual approval gate (the `production` GitHub Environment) pauses the run
|
# build all targets on EAS in parallel via custom bun build configs:
|
||||||
# before any build/submit starts. Configure required reviewers on that
|
# 1. iOS phone → App Store (auto-submit)
|
||||||
# environment in repo Settings → Environments → production.
|
# 2. tvOS → App Store (auto-submit)
|
||||||
|
# 3. Android AAB → Google Play (auto-submit)
|
||||||
|
# 4. Android phone APK→ downloadable artifact
|
||||||
|
# 5. Android TV APK → downloadable artifact
|
||||||
|
# Note: EAS queues builds based on your plan's concurrency; parallel jobs
|
||||||
|
# here just submit them — EAS may still run them serially.
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: release-${{ github.ref }}
|
group: release-${{ github.ref }}
|
||||||
@@ -23,7 +28,7 @@ jobs:
|
|||||||
- name: ✅ Release approved
|
- name: ✅ Release approved
|
||||||
run: echo "Release approved for ${{ github.sha }}"
|
run: echo "Release approved for ${{ github.sha }}"
|
||||||
|
|
||||||
release:
|
build:
|
||||||
name: 🚀 ${{ matrix.name }}
|
name: 🚀 ${{ matrix.name }}
|
||||||
needs: approve
|
needs: approve
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
@@ -36,12 +41,25 @@ jobs:
|
|||||||
- name: 🍎 iOS
|
- name: 🍎 iOS
|
||||||
platform: ios
|
platform: ios
|
||||||
profile: production
|
profile: production
|
||||||
|
submit: true
|
||||||
- name: 📺 tvOS
|
- name: 📺 tvOS
|
||||||
platform: ios
|
platform: ios
|
||||||
profile: production_tv
|
profile: production_tv
|
||||||
- name: 🤖 Android
|
submit: true
|
||||||
|
- name: 🤖 Android AAB
|
||||||
platform: android
|
platform: android
|
||||||
profile: production
|
profile: production
|
||||||
|
submit: true
|
||||||
|
- name: 🤖 Android APK
|
||||||
|
platform: android
|
||||||
|
profile: production-apk
|
||||||
|
submit: false
|
||||||
|
artifact_name: streamyfin-android-phone-apk
|
||||||
|
- name: 📺 Android TV APK
|
||||||
|
platform: android
|
||||||
|
profile: production-apk-tv
|
||||||
|
submit: false
|
||||||
|
artifact_name: streamyfin-android-tv-apk
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: 📥 Checkout code
|
- name: 📥 Checkout code
|
||||||
@@ -76,10 +94,8 @@ jobs:
|
|||||||
token: ${{ secrets.EXPO_TOKEN }}
|
token: ${{ secrets.EXPO_TOKEN }}
|
||||||
eas-cache: true
|
eas-cache: true
|
||||||
|
|
||||||
# tvOS uses local credentials (EAS can't manage tvOS provisioning
|
# tvOS uses credentialsSource: local — restore the gitignored
|
||||||
# remotely, including the TopShelf extension target). Restore the
|
# credentials.json + cert + provisioning profiles from secrets.
|
||||||
# gitignored credentials.json + cert + profiles from secrets so the
|
|
||||||
# cloud build can sign with `credentialsSource: local`.
|
|
||||||
- name: 🔐 Restore tvOS signing credentials
|
- name: 🔐 Restore tvOS signing credentials
|
||||||
if: matrix.profile == 'production_tv'
|
if: matrix.profile == 'production_tv'
|
||||||
env:
|
env:
|
||||||
@@ -94,10 +110,14 @@ jobs:
|
|||||||
echo "$TVOS_APP_PROFILE_BASE64" | base64 -d > profiles/Streamyfin_tvOS_App_Store.mobileprovision
|
echo "$TVOS_APP_PROFILE_BASE64" | base64 -d > profiles/Streamyfin_tvOS_App_Store.mobileprovision
|
||||||
echo "$TVOS_TOPSHELF_PROFILE_BASE64" | base64 -d > profiles/Streamyfin_TopShelf_tvOS_App_Store.mobileprovision
|
echo "$TVOS_TOPSHELF_PROFILE_BASE64" | base64 -d > profiles/Streamyfin_TopShelf_tvOS_App_Store.mobileprovision
|
||||||
|
|
||||||
# iOS + tvOS submit upload to App Store Connect with an ASC API key.
|
# Android Play submit needs the Google Play service account JSON.
|
||||||
# EAS reads it from EXPO_ASC_API_KEY_PATH / EXPO_ASC_KEY_ID /
|
- name: 🔐 Restore Google Play service account
|
||||||
# EXPO_ASC_ISSUER_ID (set on the build step below). Write the .p8,
|
if: matrix.platform == 'android' && matrix.submit
|
||||||
# tolerating either raw-PEM or base64-encoded secret content.
|
env:
|
||||||
|
GOOGLE_SERVICE_ACCOUNT_KEY: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }}
|
||||||
|
run: printf '%s' "$GOOGLE_SERVICE_ACCOUNT_KEY" > google-service-account.json
|
||||||
|
|
||||||
|
# App Store Connect API key for iOS/tvOS submit (raw-PEM or base64).
|
||||||
- name: 🔐 Restore App Store Connect API key
|
- name: 🔐 Restore App Store Connect API key
|
||||||
if: matrix.platform == 'ios'
|
if: matrix.platform == 'ios'
|
||||||
env:
|
env:
|
||||||
@@ -109,18 +129,11 @@ jobs:
|
|||||||
printf '%s' "$APPLE_KEY_CONTENT" | base64 -d > "$RUNNER_TEMP/asc_api_key.p8"
|
printf '%s' "$APPLE_KEY_CONTENT" | base64 -d > "$RUNNER_TEMP/asc_api_key.p8"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Android submit needs a Google Play service account JSON. eas.json's
|
# ── Submit builds: cloud build + auto-submit to the store ──
|
||||||
# submit.production.android.serviceAccountKeyPath points at this file.
|
|
||||||
- name: 🔐 Restore Google Play service account
|
|
||||||
if: matrix.platform == 'android'
|
|
||||||
env:
|
|
||||||
GOOGLE_SERVICE_ACCOUNT_KEY: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }}
|
|
||||||
run: printf '%s' "$GOOGLE_SERVICE_ACCOUNT_KEY" > google-service-account.json
|
|
||||||
|
|
||||||
- name: 🚀 Build & submit (${{ matrix.name }})
|
- name: 🚀 Build & submit (${{ matrix.name }})
|
||||||
|
if: matrix.submit
|
||||||
env:
|
env:
|
||||||
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
|
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
|
||||||
# Consumed by eas submit for iOS/tvOS; ignored for Android.
|
|
||||||
EXPO_ASC_API_KEY_PATH: ${{ runner.temp }}/asc_api_key.p8
|
EXPO_ASC_API_KEY_PATH: ${{ runner.temp }}/asc_api_key.p8
|
||||||
EXPO_ASC_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
|
EXPO_ASC_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
|
||||||
EXPO_ASC_ISSUER_ID: ${{ secrets.APPLE_KEY_ISSUER_ID }}
|
EXPO_ASC_ISSUER_ID: ${{ secrets.APPLE_KEY_ISSUER_ID }}
|
||||||
@@ -129,4 +142,75 @@ jobs:
|
|||||||
--platform ${{ matrix.platform }} \
|
--platform ${{ matrix.platform }} \
|
||||||
--profile ${{ matrix.profile }} \
|
--profile ${{ matrix.profile }} \
|
||||||
--auto-submit \
|
--auto-submit \
|
||||||
--non-interactive
|
--non-interactive \
|
||||||
|
--wait
|
||||||
|
|
||||||
|
# ── Artifact builds: cloud build, then download + upload the APK ──
|
||||||
|
- name: 🏗️ Build artifact (${{ matrix.name }})
|
||||||
|
if: ${{ !matrix.submit }}
|
||||||
|
env:
|
||||||
|
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
|
||||||
|
run: |
|
||||||
|
eas build \
|
||||||
|
--platform ${{ matrix.platform }} \
|
||||||
|
--profile ${{ matrix.profile }} \
|
||||||
|
--non-interactive \
|
||||||
|
--wait \
|
||||||
|
--json > build-result.json
|
||||||
|
URL=$(node -e "const b=require('./build-result.json'); const x=Array.isArray(b)?b[0]:b; console.log(x.artifacts.applicationArchiveUrl)")
|
||||||
|
echo "Downloading artifact: $URL"
|
||||||
|
curl -fL "$URL" -o "${{ matrix.artifact_name }}.apk"
|
||||||
|
|
||||||
|
- name: 📤 Upload APK artifact (${{ matrix.name }})
|
||||||
|
if: ${{ !matrix.submit }}
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.artifact_name }}
|
||||||
|
path: ${{ matrix.artifact_name }}.apk
|
||||||
|
retention-days: 14
|
||||||
|
|
||||||
|
# Draft a GitHub Release with the two APKs attached. The tag comes from the
|
||||||
|
# merged-in app version (app.json → expo.version), NOT the auto-incremented
|
||||||
|
# build number — so cutting a release is a deliberate version bump via PR.
|
||||||
|
github-release:
|
||||||
|
name: 📦 Draft GitHub Release
|
||||||
|
needs: build
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
actions: read # required for `gh run download` to list/fetch this run's artifacts
|
||||||
|
steps:
|
||||||
|
- name: 📥 Checkout code
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
show-progress: false
|
||||||
|
|
||||||
|
- name: 📦 Download APK artifacts from this run
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
mkdir -p apks
|
||||||
|
gh run download ${{ github.run_id }} --name streamyfin-android-phone-apk --dir apks
|
||||||
|
gh run download ${{ github.run_id }} --name streamyfin-android-tv-apk --dir apks
|
||||||
|
ls -la apks
|
||||||
|
|
||||||
|
- name: 📝 Draft release (tag = app.json version, not auto-bumped)
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
VERSION=$(node -e "console.log(require('./app.json').expo.version)")
|
||||||
|
TAG="v$VERSION"
|
||||||
|
echo "Release tag from merged app version: $TAG"
|
||||||
|
if gh release view "$TAG" >/dev/null 2>&1; then
|
||||||
|
echo "Release $TAG exists — updating APK assets"
|
||||||
|
gh release upload "$TAG" apks/*.apk --clobber
|
||||||
|
else
|
||||||
|
echo "Creating draft release $TAG"
|
||||||
|
gh release create "$TAG" \
|
||||||
|
--draft \
|
||||||
|
--generate-notes \
|
||||||
|
--title "$TAG" \
|
||||||
|
apks/*.apk
|
||||||
|
fi
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -76,6 +76,9 @@ modules/background-downloader/android/build/*
|
|||||||
|
|
||||||
# ios:unsigned-build Artifacts
|
# ios:unsigned-build Artifacts
|
||||||
build/
|
build/
|
||||||
|
# but keep EAS custom build configs (the generic build/ rule above matches .eas/build/)
|
||||||
|
!.eas/build/
|
||||||
|
!.eas/build/**
|
||||||
.claude/
|
.claude/
|
||||||
.agents/skills/**
|
.agents/skills/**
|
||||||
skills-lock.json
|
skills-lock.json
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageSta
|
|||||||
import useRouter from "@/hooks/useAppRouter";
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
|
|
||||||
const Chromecast = Platform.isTV ? null : require("@/components/Chromecast");
|
const Chromecast = Platform.isTV ? null : require("@/components/Chromecast");
|
||||||
|
const SyncPlayButtonComponent = Platform.isTV
|
||||||
|
? null
|
||||||
|
: require("@/components/syncplay/SyncPlayButton").SyncPlayButton;
|
||||||
|
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { HeaderBackButton } from "@/components/common/HeaderBackButton";
|
import { HeaderBackButton } from "@/components/common/HeaderBackButton";
|
||||||
@@ -33,6 +36,7 @@ export default function IndexLayout() {
|
|||||||
{!Platform.isTV && (
|
{!Platform.isTV && (
|
||||||
<>
|
<>
|
||||||
<Chromecast.Chromecast background='transparent' />
|
<Chromecast.Chromecast background='transparent' />
|
||||||
|
{SyncPlayButtonComponent && <SyncPlayButtonComponent />}
|
||||||
{user?.Policy?.IsAdministrator && <SessionsButton />}
|
{user?.Policy?.IsAdministrator && <SessionsButton />}
|
||||||
<SettingsButton />
|
<SettingsButton />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -59,17 +59,19 @@ function SettingsMobile() {
|
|||||||
|
|
||||||
<QuickConnect className='mb-4' />
|
<QuickConnect className='mb-4' />
|
||||||
|
|
||||||
<View className='mb-4'>
|
{Platform.OS !== "ios" && (
|
||||||
<ListGroup title={t("pairing.pair_with_phone_title")}>
|
<View className='mb-4'>
|
||||||
<ListItem
|
<ListGroup title={t("pairing.pair_with_phone_title")}>
|
||||||
onPress={() =>
|
<ListItem
|
||||||
router.push("/(auth)/(tabs)/(home)/companion-login")
|
onPress={() =>
|
||||||
}
|
router.push("/(auth)/(tabs)/(home)/companion-login")
|
||||||
title={t("pairing.pair_with_phone")}
|
}
|
||||||
textColor='blue'
|
title={t("pairing.pair_with_phone")}
|
||||||
/>
|
textColor='blue'
|
||||||
</ListGroup>
|
/>
|
||||||
</View>
|
</ListGroup>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
<View className='mb-4'>
|
<View className='mb-4'>
|
||||||
<AppLanguageSelector />
|
<AppLanguageSelector />
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export default function StreamystatsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRefreshFromServer = useCallback(async () => {
|
const handleRefreshFromServer = useCallback(async () => {
|
||||||
const newPluginSettings = await refreshStreamyfinPluginSettings(true);
|
const newPluginSettings = await refreshStreamyfinPluginSettings();
|
||||||
// Update local state with new values
|
// Update local state with new values
|
||||||
const newUrl = newPluginSettings?.streamyStatsServerUrl?.value || "";
|
const newUrl = newPluginSettings?.streamyStatsServerUrl?.value || "";
|
||||||
setUrl(newUrl);
|
setUrl(newUrl);
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ export default function IndexLayout() {
|
|||||||
open={dropdownOpen}
|
open={dropdownOpen}
|
||||||
onOpenChange={setDropdownOpen}
|
onOpenChange={setDropdownOpen}
|
||||||
trigger={
|
trigger={
|
||||||
<View className='pl-1.5'>
|
<View>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name='ellipsis-horizontal-outline'
|
name='ellipsis-horizontal-outline'
|
||||||
size={24}
|
size={24}
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ const Layout = () => {
|
|||||||
keyboardDismissMode='none'
|
keyboardDismissMode='none'
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
tabBarBounces: true,
|
tabBarBounces: true,
|
||||||
|
tabBarActiveTintColor: "#FFFFFF",
|
||||||
|
tabBarInactiveTintColor: "#9CA3AF",
|
||||||
tabBarLabelStyle: {
|
tabBarLabelStyle: {
|
||||||
fontSize: TAB_LABEL_FONT_SIZE,
|
fontSize: TAB_LABEL_FONT_SIZE,
|
||||||
fontWeight: "600",
|
fontWeight: "600",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
} from "@/components/video-player/controls/utils/playback-speed-settings";
|
} from "@/components/video-player/controls/utils/playback-speed-settings";
|
||||||
import useRouter from "@/hooks/useAppRouter";
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
|
import { useKeepWebSocketAlive } from "@/hooks/useKeepWebSocketAlive";
|
||||||
import { useOrientation } from "@/hooks/useOrientation";
|
import { useOrientation } from "@/hooks/useOrientation";
|
||||||
import { usePlaybackManager } from "@/hooks/usePlaybackManager";
|
import { usePlaybackManager } from "@/hooks/usePlaybackManager";
|
||||||
import usePlaybackSpeed from "@/hooks/usePlaybackSpeed";
|
import usePlaybackSpeed from "@/hooks/usePlaybackSpeed";
|
||||||
@@ -49,7 +50,8 @@ import { DownloadedItem } from "@/providers/Downloads/types";
|
|||||||
import { useInactivity } from "@/providers/InactivityProvider";
|
import { useInactivity } from "@/providers/InactivityProvider";
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
import { OfflineModeProvider } from "@/providers/OfflineModeProvider";
|
import { OfflineModeProvider } from "@/providers/OfflineModeProvider";
|
||||||
|
import { useSyncPlay } from "@/providers/SyncPlay";
|
||||||
|
import type { PlayerControls } from "@/providers/SyncPlay/types";
|
||||||
import { getSubtitlesForItem } from "@/utils/atoms/downloadedSubtitles";
|
import { getSubtitlesForItem } from "@/utils/atoms/downloadedSubtitles";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
|
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
|
||||||
@@ -77,6 +79,11 @@ export default function DirectPlayerPage() {
|
|||||||
const [isPlaybackStopped, setIsPlaybackStopped] = useState(false);
|
const [isPlaybackStopped, setIsPlaybackStopped] = useState(false);
|
||||||
const [showControls, _setShowControls] = useState(true);
|
const [showControls, _setShowControls] = useState(true);
|
||||||
const [isPipMode, setIsPipMode] = useState(false);
|
const [isPipMode, setIsPipMode] = useState(false);
|
||||||
|
|
||||||
|
// Keep the global WebSocket open while in PiP so SyncPlay commands
|
||||||
|
// (and any other server pushes) keep flowing while iOS treats the
|
||||||
|
// app as backgrounded. See `WebSocketProvider.acquireKeepAlive`.
|
||||||
|
useKeepWebSocketAlive(isPipMode);
|
||||||
const [aspectRatio] = useState<"default" | "16:9" | "4:3" | "1:1" | "21:9">(
|
const [aspectRatio] = useState<"default" | "16:9" | "4:3" | "1:1" | "21:9">(
|
||||||
"default",
|
"default",
|
||||||
);
|
);
|
||||||
@@ -128,6 +135,7 @@ export default function DirectPlayerPage() {
|
|||||||
bitrateValue: bitrateValueStr,
|
bitrateValue: bitrateValueStr,
|
||||||
offline: offlineStr,
|
offline: offlineStr,
|
||||||
playbackPosition: playbackPositionFromUrl,
|
playbackPosition: playbackPositionFromUrl,
|
||||||
|
syncPlay: syncPlayStr,
|
||||||
} = useLocalSearchParams<{
|
} = useLocalSearchParams<{
|
||||||
itemId: string;
|
itemId: string;
|
||||||
audioIndex: string;
|
audioIndex: string;
|
||||||
@@ -137,9 +145,23 @@ export default function DirectPlayerPage() {
|
|||||||
offline: string;
|
offline: string;
|
||||||
/** Playback position in ticks. */
|
/** Playback position in ticks. */
|
||||||
playbackPosition?: string;
|
playbackPosition?: string;
|
||||||
|
/** Whether playback was initiated by SyncPlay */
|
||||||
|
syncPlay?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
// When opened via SyncPlay, don't auto-play - let SyncPlay commands control playback
|
||||||
|
const openedViaSyncPlay = syncPlayStr === "true";
|
||||||
const { lockOrientation, unlockOrientation } = useOrientation();
|
const { lockOrientation, unlockOrientation } = useOrientation();
|
||||||
|
|
||||||
|
// SyncPlay integration
|
||||||
|
const syncPlay = useSyncPlay();
|
||||||
|
const {
|
||||||
|
isEnabled: isSyncPlayEnabled,
|
||||||
|
controller: syncPlayController,
|
||||||
|
setPlayerControls,
|
||||||
|
notifyBuffering,
|
||||||
|
} = syncPlay;
|
||||||
|
|
||||||
const offline = offlineStr === "true";
|
const offline = offlineStr === "true";
|
||||||
|
|
||||||
// Audio index: use URL param if provided, otherwise use stored index for offline playback
|
// Audio index: use URL param if provided, otherwise use stored index for offline playback
|
||||||
@@ -274,6 +296,11 @@ export default function DirectPlayerPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (itemId) {
|
if (itemId) {
|
||||||
|
setItem(null);
|
||||||
|
setDownloadedItem(null);
|
||||||
|
// Clear the previous episode's stream so the loader gate stays closed
|
||||||
|
// until the new item's stream resolves (avoids a stale MPV source frame).
|
||||||
|
setStream(null);
|
||||||
fetchItemData();
|
fetchItemData();
|
||||||
}
|
}
|
||||||
}, [itemId, offline, api, user?.Id]);
|
}, [itemId, offline, api, user?.Id]);
|
||||||
@@ -316,6 +343,12 @@ export default function DirectPlayerPage() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure item matches the current itemId to avoid race conditions
|
||||||
|
if (item.Id !== itemId) {
|
||||||
|
setStreamStatus({ isLoading: false, isError: false });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
let result: Stream | null = null;
|
let result: Stream | null = null;
|
||||||
if (offline && downloadedItem?.mediaSource) {
|
if (offline && downloadedItem?.mediaSource) {
|
||||||
const url = downloadedItem.videoFilePath;
|
const url = downloadedItem.videoFilePath;
|
||||||
@@ -388,6 +421,7 @@ export default function DirectPlayerPage() {
|
|||||||
item,
|
item,
|
||||||
user?.Id,
|
user?.Id,
|
||||||
downloadedItem,
|
downloadedItem,
|
||||||
|
offline,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -403,8 +437,72 @@ export default function DirectPlayerPage() {
|
|||||||
reportPlaybackStart();
|
reportPlaybackStart();
|
||||||
}, [stream, api, offline]);
|
}, [stream, api, offline]);
|
||||||
|
|
||||||
|
// SyncPlay: Connect player controls when video is ready
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isVideoLoaded || !videoRef.current || offline) {
|
||||||
|
setPlayerControls(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const controls: PlayerControls = {
|
||||||
|
play: () => videoRef.current?.play(),
|
||||||
|
pause: () => videoRef.current?.pause(),
|
||||||
|
seekTo: (positionMs: number) => {
|
||||||
|
const positionSec = positionMs / 1000;
|
||||||
|
console.log(
|
||||||
|
`PlayerControls.seekTo: ${positionMs}ms = ${positionSec}s, videoRef exists: ${!!videoRef.current}`,
|
||||||
|
);
|
||||||
|
videoRef.current?.seekTo(positionSec);
|
||||||
|
},
|
||||||
|
setSpeed: (speed: number) => videoRef.current?.setSpeed?.(speed),
|
||||||
|
getSpeed: () => currentPlaybackSpeed,
|
||||||
|
getCurrentPosition: () => progress.get(),
|
||||||
|
isPlaying: () => isPlaying,
|
||||||
|
isBuffering: () => isBuffering,
|
||||||
|
};
|
||||||
|
|
||||||
|
setPlayerControls(controls);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setPlayerControls(null);
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
isVideoLoaded,
|
||||||
|
offline,
|
||||||
|
isPlaying,
|
||||||
|
isBuffering,
|
||||||
|
currentPlaybackSpeed,
|
||||||
|
progress,
|
||||||
|
setPlayerControls,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// SyncPlay: Report buffering/ready state to server.
|
||||||
|
//
|
||||||
|
// CRITICAL: We must report `buffering` to the server *during* initial
|
||||||
|
// load (before `isVideoLoaded`), otherwise the server treats us as ready
|
||||||
|
// and proceeds without waiting for us. jellyfin-web reports this for
|
||||||
|
// free via the HTML5 video element's `waiting` event; for us, the
|
||||||
|
// initial load itself is the buffering window.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isSyncPlayEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLocallyReady = isVideoLoaded && !isBuffering;
|
||||||
|
// notifyBuffering routes through the debouncer in PlaybackCore so
|
||||||
|
// re-renders during a stall don't spam the server.
|
||||||
|
notifyBuffering(!isLocallyReady);
|
||||||
|
}, [isSyncPlayEnabled, isVideoLoaded, isBuffering, notifyBuffering]);
|
||||||
|
|
||||||
const togglePlay = async () => {
|
const togglePlay = async () => {
|
||||||
lightHapticFeedback();
|
lightHapticFeedback();
|
||||||
|
|
||||||
|
// Route through SyncPlay when active
|
||||||
|
if (isSyncPlayEnabled && syncPlayController) {
|
||||||
|
syncPlayController.playPause();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsPlaying(!isPlaying);
|
setIsPlaying(!isPlaying);
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
await videoRef.current?.pause();
|
await videoRef.current?.pause();
|
||||||
@@ -427,21 +525,15 @@ export default function DirectPlayerPage() {
|
|||||||
if (!item?.Id || !stream?.sessionId || offline || !api) return;
|
if (!item?.Id || !stream?.sessionId || offline || !api) return;
|
||||||
|
|
||||||
const currentTimeInTicks = msToTicks(progress.get());
|
const currentTimeInTicks = msToTicks(progress.get());
|
||||||
await getPlaystateApi(api).onPlaybackStopped({
|
await getPlaystateApi(api).reportPlaybackStopped({
|
||||||
itemId: item.Id,
|
playbackStopInfo: {
|
||||||
mediaSourceId: mediaSourceId,
|
ItemId: item.Id,
|
||||||
positionTicks: currentTimeInTicks,
|
MediaSourceId: mediaSourceId,
|
||||||
playSessionId: stream.sessionId,
|
PositionTicks: currentTimeInTicks,
|
||||||
|
PlaySessionId: stream.sessionId,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}, [
|
}, [api, item, mediaSourceId, stream, progress, offline]);
|
||||||
api,
|
|
||||||
item,
|
|
||||||
mediaSourceId,
|
|
||||||
stream,
|
|
||||||
progress,
|
|
||||||
offline,
|
|
||||||
revalidateProgressCache,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const stop = useCallback(() => {
|
const stop = useCallback(() => {
|
||||||
// Update URL with final playback position before stopping
|
// Update URL with final playback position before stopping
|
||||||
@@ -459,9 +551,10 @@ export default function DirectPlayerPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const beforeRemoveListener = navigation.addListener("beforeRemove", stop);
|
const beforeRemoveListener = navigation.addListener("beforeRemove", stop);
|
||||||
return () => {
|
return () => {
|
||||||
|
reportPlaybackStopped();
|
||||||
beforeRemoveListener();
|
beforeRemoveListener();
|
||||||
};
|
};
|
||||||
}, [navigation, stop]);
|
}, [navigation, stop, reportPlaybackStopped]);
|
||||||
|
|
||||||
const currentPlayStateInfo = useCallback(():
|
const currentPlayStateInfo = useCallback(():
|
||||||
| PlaybackProgressInfo
|
| PlaybackProgressInfo
|
||||||
@@ -638,10 +731,12 @@ export default function DirectPlayerPage() {
|
|||||||
const startPos = ticksToSeconds(startTicks);
|
const startPos = ticksToSeconds(startTicks);
|
||||||
|
|
||||||
// Build source config - headers only needed for online streaming
|
// Build source config - headers only needed for online streaming
|
||||||
|
// When opened via SyncPlay, don't auto-play - SyncPlay commands control playback
|
||||||
|
const shouldAutoplay = !openedViaSyncPlay;
|
||||||
const source: MpvVideoSource = {
|
const source: MpvVideoSource = {
|
||||||
url: stream.url,
|
url: stream.url,
|
||||||
startPosition: startPos,
|
startPosition: startPos,
|
||||||
autoplay: true,
|
autoplay: shouldAutoplay,
|
||||||
initialSubtitleId,
|
initialSubtitleId,
|
||||||
initialAudioId,
|
initialAudioId,
|
||||||
// Pass cache/buffer settings from user preferences
|
// Pass cache/buffer settings from user preferences
|
||||||
@@ -836,6 +931,41 @@ export default function DirectPlayerPage() {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// PiP playback controls. When SyncPlay is active, the native side
|
||||||
|
// is told to *delegate* these via `syncPlayDelegated`, so the OS
|
||||||
|
// play/pause/skip buttons emit these events instead of poking MPV
|
||||||
|
// directly. We route them through the SyncPlay controller so the
|
||||||
|
// server broadcasts a command to every group member (including us).
|
||||||
|
const _onPipPlayRequest = useCallback(() => {
|
||||||
|
if (isSyncPlayEnabled && syncPlayController) {
|
||||||
|
console.log("SyncPlay: PiP play → controller.playPause()");
|
||||||
|
syncPlayController.playPause();
|
||||||
|
}
|
||||||
|
}, [isSyncPlayEnabled, syncPlayController]);
|
||||||
|
|
||||||
|
const _onPipPauseRequest = useCallback(() => {
|
||||||
|
if (isSyncPlayEnabled && syncPlayController) {
|
||||||
|
console.log("SyncPlay: PiP pause → controller.playPause()");
|
||||||
|
syncPlayController.playPause();
|
||||||
|
}
|
||||||
|
}, [isSyncPlayEnabled, syncPlayController]);
|
||||||
|
|
||||||
|
const _onPipSkipRequest = useCallback(
|
||||||
|
(e: {
|
||||||
|
nativeEvent: { targetSeconds: number; intervalSeconds: number };
|
||||||
|
}) => {
|
||||||
|
if (!isSyncPlayEnabled || !syncPlayController) return;
|
||||||
|
const { targetSeconds } = e.nativeEvent;
|
||||||
|
// SyncPlay seek takes ticks (1 s = 10_000_000 ticks).
|
||||||
|
const ticks = Math.max(0, Math.round(targetSeconds * 10_000_000));
|
||||||
|
console.log(
|
||||||
|
`SyncPlay: PiP skip → controller.seek(${targetSeconds}s = ${ticks} ticks)`,
|
||||||
|
);
|
||||||
|
syncPlayController.seek(ticks);
|
||||||
|
},
|
||||||
|
[isSyncPlayEnabled, syncPlayController],
|
||||||
|
);
|
||||||
|
|
||||||
const [isMounted, setIsMounted] = useState(false);
|
const [isMounted, setIsMounted] = useState(false);
|
||||||
|
|
||||||
// Add useEffect to handle mounting
|
// Add useEffect to handle mounting
|
||||||
@@ -860,10 +990,21 @@ export default function DirectPlayerPage() {
|
|||||||
videoRef.current?.pause?.();
|
videoRef.current?.pause?.();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const seek = useCallback((position: number) => {
|
const seek = useCallback(
|
||||||
// MPV expects seconds, convert from ms
|
(position: number) => {
|
||||||
videoRef.current?.seekTo?.(position / 1000);
|
// Route through SyncPlay when active. `position` is in ms; the
|
||||||
}, []);
|
// controller takes ticks (1 ms = 10000 ticks).
|
||||||
|
if (isSyncPlayEnabled && syncPlayController) {
|
||||||
|
console.log("SyncPlay: seek requested via SyncPlay", position);
|
||||||
|
syncPlayController.seek(Math.round(position * 10000));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MPV expects seconds, convert from ms
|
||||||
|
videoRef.current?.seekTo?.(position / 1000);
|
||||||
|
},
|
||||||
|
[isSyncPlayEnabled, syncPlayController],
|
||||||
|
);
|
||||||
|
|
||||||
// TV audio track change handler
|
// TV audio track change handler
|
||||||
const handleAudioIndexChange = useCallback(
|
const handleAudioIndexChange = useCallback(
|
||||||
@@ -1255,6 +1396,10 @@ export default function DirectPlayerPage() {
|
|||||||
onProgress={onProgress}
|
onProgress={onProgress}
|
||||||
onPlaybackStateChange={onPlaybackStateChanged}
|
onPlaybackStateChange={onPlaybackStateChanged}
|
||||||
onPictureInPictureChange={_onPictureInPictureChange}
|
onPictureInPictureChange={_onPictureInPictureChange}
|
||||||
|
syncPlayDelegated={isSyncPlayEnabled}
|
||||||
|
onPipPlayRequest={_onPipPlayRequest}
|
||||||
|
onPipPauseRequest={_onPipPauseRequest}
|
||||||
|
onPipSkipRequest={_onPipSkipRequest}
|
||||||
onLoad={() => setIsVideoLoaded(true)}
|
onLoad={() => setIsVideoLoaded(true)}
|
||||||
onError={(e: { nativeEvent: MpvOnErrorEventPayload }) => {
|
onError={(e: { nativeEvent: MpvOnErrorEventPayload }) => {
|
||||||
console.error("Video Error:", e.nativeEvent);
|
console.error("Video Error:", e.nativeEvent);
|
||||||
|
|||||||
254
app/_layout.tsx
254
app/_layout.tsx
@@ -25,6 +25,7 @@ import { MusicPlayerProvider } from "@/providers/MusicPlayerProvider";
|
|||||||
import { NetworkStatusProvider } from "@/providers/NetworkStatusProvider";
|
import { NetworkStatusProvider } from "@/providers/NetworkStatusProvider";
|
||||||
import { PlaySettingsProvider } from "@/providers/PlaySettingsProvider";
|
import { PlaySettingsProvider } from "@/providers/PlaySettingsProvider";
|
||||||
import { ServerUrlProvider } from "@/providers/ServerUrlProvider";
|
import { ServerUrlProvider } from "@/providers/ServerUrlProvider";
|
||||||
|
import { SyncPlayProvider } from "@/providers/SyncPlay";
|
||||||
import { WebSocketProvider } from "@/providers/WebSocketProvider";
|
import { WebSocketProvider } from "@/providers/WebSocketProvider";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import {
|
import {
|
||||||
@@ -409,133 +410,136 @@ function Layout() {
|
|||||||
<PlaySettingsProvider>
|
<PlaySettingsProvider>
|
||||||
<LogProvider>
|
<LogProvider>
|
||||||
<WebSocketProvider>
|
<WebSocketProvider>
|
||||||
<DownloadProvider>
|
<SyncPlayProvider>
|
||||||
<MusicPlayerProvider>
|
<DownloadProvider>
|
||||||
<GlobalModalProvider>
|
<MusicPlayerProvider>
|
||||||
<BottomSheetModalProvider>
|
<GlobalModalProvider>
|
||||||
<IntroSheetProvider>
|
<BottomSheetModalProvider>
|
||||||
<ThemeProvider value={DarkTheme}>
|
<IntroSheetProvider>
|
||||||
<SystemBars style='light' hidden={false} />
|
<ThemeProvider value={DarkTheme}>
|
||||||
<Stack initialRouteName='(auth)/(tabs)'>
|
<SystemBars style='light' hidden={false} />
|
||||||
<Stack.Screen
|
<Stack initialRouteName='(auth)/(tabs)'>
|
||||||
name='(auth)/(tabs)'
|
<Stack.Screen
|
||||||
options={{
|
name='(auth)/(tabs)'
|
||||||
headerShown: false,
|
options={{
|
||||||
title: "",
|
headerShown: false,
|
||||||
header: () => null,
|
title: "",
|
||||||
|
header: () => null,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='(auth)/player'
|
||||||
|
options={{
|
||||||
|
headerShown: false,
|
||||||
|
title: "",
|
||||||
|
header: () => null,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='(auth)/now-playing'
|
||||||
|
options={{
|
||||||
|
headerShown: false,
|
||||||
|
presentation: "modal",
|
||||||
|
gestureEnabled: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='login'
|
||||||
|
options={{
|
||||||
|
headerShown: true,
|
||||||
|
title: "",
|
||||||
|
headerTransparent:
|
||||||
|
Platform.OS === "ios",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen name='+not-found' />
|
||||||
|
<Stack.Screen
|
||||||
|
name='(auth)/tv-option-modal'
|
||||||
|
options={{
|
||||||
|
headerShown: false,
|
||||||
|
presentation: "transparentModal",
|
||||||
|
animation: "fade",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='(auth)/tv-subtitle-modal'
|
||||||
|
options={{
|
||||||
|
headerShown: false,
|
||||||
|
presentation: "transparentModal",
|
||||||
|
animation: "fade",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='(auth)/tv-request-modal'
|
||||||
|
options={{
|
||||||
|
headerShown: false,
|
||||||
|
presentation: "transparentModal",
|
||||||
|
animation: "fade",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='(auth)/tv-season-select-modal'
|
||||||
|
options={{
|
||||||
|
headerShown: false,
|
||||||
|
presentation: "transparentModal",
|
||||||
|
animation: "fade",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='(auth)/tv-series-season-modal'
|
||||||
|
options={{
|
||||||
|
headerShown: false,
|
||||||
|
presentation: "transparentModal",
|
||||||
|
animation: "fade",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='tv-account-action-modal'
|
||||||
|
options={{
|
||||||
|
headerShown: false,
|
||||||
|
presentation: "transparentModal",
|
||||||
|
animation: "fade",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='tv-account-select-modal'
|
||||||
|
options={{
|
||||||
|
headerShown: false,
|
||||||
|
presentation: "transparentModal",
|
||||||
|
animation: "fade",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='(auth)/tv-user-switch-modal'
|
||||||
|
options={{
|
||||||
|
headerShown: false,
|
||||||
|
presentation: "transparentModal",
|
||||||
|
animation: "fade",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Toaster
|
||||||
|
duration={4000}
|
||||||
|
toastOptions={{
|
||||||
|
style: {
|
||||||
|
backgroundColor: "#262626",
|
||||||
|
borderColor: "#363639",
|
||||||
|
borderWidth: 1,
|
||||||
|
},
|
||||||
|
titleStyle: {
|
||||||
|
color: "white",
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
|
closeButton
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
{!Platform.isTV && <GlobalModal />}
|
||||||
name='(auth)/player'
|
</ThemeProvider>
|
||||||
options={{
|
</IntroSheetProvider>
|
||||||
headerShown: false,
|
</BottomSheetModalProvider>
|
||||||
title: "",
|
</GlobalModalProvider>
|
||||||
header: () => null,
|
</MusicPlayerProvider>
|
||||||
}}
|
</DownloadProvider>
|
||||||
/>
|
</SyncPlayProvider>
|
||||||
<Stack.Screen
|
|
||||||
name='(auth)/now-playing'
|
|
||||||
options={{
|
|
||||||
headerShown: false,
|
|
||||||
presentation: "modal",
|
|
||||||
gestureEnabled: true,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name='login'
|
|
||||||
options={{
|
|
||||||
headerShown: true,
|
|
||||||
title: "",
|
|
||||||
headerTransparent: Platform.OS === "ios",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen name='+not-found' />
|
|
||||||
<Stack.Screen
|
|
||||||
name='(auth)/tv-option-modal'
|
|
||||||
options={{
|
|
||||||
headerShown: false,
|
|
||||||
presentation: "transparentModal",
|
|
||||||
animation: "fade",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name='(auth)/tv-subtitle-modal'
|
|
||||||
options={{
|
|
||||||
headerShown: false,
|
|
||||||
presentation: "transparentModal",
|
|
||||||
animation: "fade",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name='(auth)/tv-request-modal'
|
|
||||||
options={{
|
|
||||||
headerShown: false,
|
|
||||||
presentation: "transparentModal",
|
|
||||||
animation: "fade",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name='(auth)/tv-season-select-modal'
|
|
||||||
options={{
|
|
||||||
headerShown: false,
|
|
||||||
presentation: "transparentModal",
|
|
||||||
animation: "fade",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name='(auth)/tv-series-season-modal'
|
|
||||||
options={{
|
|
||||||
headerShown: false,
|
|
||||||
presentation: "transparentModal",
|
|
||||||
animation: "fade",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name='tv-account-action-modal'
|
|
||||||
options={{
|
|
||||||
headerShown: false,
|
|
||||||
presentation: "transparentModal",
|
|
||||||
animation: "fade",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name='tv-account-select-modal'
|
|
||||||
options={{
|
|
||||||
headerShown: false,
|
|
||||||
presentation: "transparentModal",
|
|
||||||
animation: "fade",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name='(auth)/tv-user-switch-modal'
|
|
||||||
options={{
|
|
||||||
headerShown: false,
|
|
||||||
presentation: "transparentModal",
|
|
||||||
animation: "fade",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
<Toaster
|
|
||||||
duration={4000}
|
|
||||||
toastOptions={{
|
|
||||||
style: {
|
|
||||||
backgroundColor: "#262626",
|
|
||||||
borderColor: "#363639",
|
|
||||||
borderWidth: 1,
|
|
||||||
},
|
|
||||||
titleStyle: {
|
|
||||||
color: "white",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
closeButton
|
|
||||||
/>
|
|
||||||
{!Platform.isTV && <GlobalModal />}
|
|
||||||
</ThemeProvider>
|
|
||||||
</IntroSheetProvider>
|
|
||||||
</BottomSheetModalProvider>
|
|
||||||
</GlobalModalProvider>
|
|
||||||
</MusicPlayerProvider>
|
|
||||||
</DownloadProvider>
|
|
||||||
</WebSocketProvider>
|
</WebSocketProvider>
|
||||||
</LogProvider>
|
</LogProvider>
|
||||||
</PlaySettingsProvider>
|
</PlaySettingsProvider>
|
||||||
|
|||||||
1
bun.lock
1
bun.lock
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"configVersion": 1,
|
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "streamyfin",
|
"name": "streamyfin",
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { BottomSheetScrollView } from "@gorhom/bottom-sheet";
|
import { BottomSheetScrollView } from "@gorhom/bottom-sheet";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect } from "react";
|
||||||
import {
|
import { Platform, StyleSheet, TouchableOpacity, View } from "react-native";
|
||||||
type LayoutChangeEvent,
|
|
||||||
Platform,
|
|
||||||
StyleSheet,
|
|
||||||
TouchableOpacity,
|
|
||||||
View,
|
|
||||||
} from "react-native";
|
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import { useGlobalModal } from "@/providers/GlobalModalProvider";
|
import { useGlobalModal } from "@/providers/GlobalModalProvider";
|
||||||
@@ -217,24 +211,6 @@ const PlatformDropdownComponent = ({
|
|||||||
}: PlatformDropdownProps) => {
|
}: PlatformDropdownProps) => {
|
||||||
const { showModal, hideModal, isVisible } = useGlobalModal();
|
const { showModal, hideModal, isVisible } = useGlobalModal();
|
||||||
|
|
||||||
// @expo/ui's <Host> (SDK 55) fills its available space by default, and
|
|
||||||
// `matchContents` doesn't help here: it reports the native Menu's size via
|
|
||||||
// setStyleSize and overrides any explicit size. Instead we measure the
|
|
||||||
// trigger's intrinsic size in plain RN (off-layout) and pin it on the Host.
|
|
||||||
const [triggerSize, setTriggerSize] = useState<{
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
} | null>(null);
|
|
||||||
|
|
||||||
const handleMeasureTrigger = (e: LayoutChangeEvent) => {
|
|
||||||
const { width, height } = e.nativeEvent.layout;
|
|
||||||
setTriggerSize((prev) =>
|
|
||||||
prev && prev.width === width && prev.height === height
|
|
||||||
? prev
|
|
||||||
: { width, height },
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle controlled open state for Android
|
// Handle controlled open state for Android
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Platform.OS === "android" && controlledOpen === true) {
|
if (Platform.OS === "android" && controlledOpen === true) {
|
||||||
@@ -265,25 +241,11 @@ const PlatformDropdownComponent = ({
|
|||||||
}, [isVisible, controlledOpen, controlledOnOpenChange]);
|
}, [isVisible, controlledOpen, controlledOnOpenChange]);
|
||||||
|
|
||||||
if (Platform.OS === "ios" && !Platform.isTV) {
|
if (Platform.OS === "ios" && !Platform.isTV) {
|
||||||
// Pin the wrapper to the measured trigger size. @expo/ui's <Host> (SDK 55)
|
// @expo/ui's <Host> can't size to content, so an in-flow invisible copy of
|
||||||
// fills its parent and reports its own size via setStyleSize, so it can't
|
// the trigger sizes the wrapper while the Host overlays the real Menu.
|
||||||
// size itself to content. If the wrapper has no size, the Host's `flex: 1`
|
|
||||||
// height depends on the parent while the parent depends on the Host — a
|
|
||||||
// circular dependency that collapses to 0 for any selector nested more than
|
|
||||||
// one level deep (so only the first, shallowest dropdown stays visible).
|
|
||||||
// Giving the wrapper the measured size breaks the cycle; the Host then
|
|
||||||
// fills a concrete box.
|
|
||||||
return (
|
return (
|
||||||
<View style={triggerSize ?? { opacity: 0 }}>
|
<View>
|
||||||
{/* Hidden measurer: lays the trigger out off-flow to capture its
|
<View pointerEvents='none' aria-hidden style={{ opacity: 0 }}>
|
||||||
intrinsic size. Absolutely positioned WITHOUT right/bottom so it
|
|
||||||
sizes to the trigger's content rather than to its parent. */}
|
|
||||||
<View
|
|
||||||
style={{ position: "absolute", top: 0, left: 0, opacity: 0 }}
|
|
||||||
pointerEvents='none'
|
|
||||||
aria-hidden
|
|
||||||
onLayout={handleMeasureTrigger}
|
|
||||||
>
|
|
||||||
{trigger}
|
{trigger}
|
||||||
</View>
|
</View>
|
||||||
<Host style={[StyleSheet.absoluteFill, expoUIConfig?.hostStyle as any]}>
|
<Host style={[StyleSheet.absoluteFill, expoUIConfig?.hostStyle as any]}>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { getDownloadedItemById } from "@/providers/Downloads/database";
|
|||||||
import { useGlobalModal } from "@/providers/GlobalModalProvider";
|
import { useGlobalModal } from "@/providers/GlobalModalProvider";
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
import { useOfflineMode } from "@/providers/OfflineModeProvider";
|
import { useOfflineMode } from "@/providers/OfflineModeProvider";
|
||||||
|
import { useSyncPlay } from "@/providers/SyncPlay/SyncPlayProvider";
|
||||||
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
|
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { getParentBackdropImageUrl } from "@/utils/jellyfin/image/getParentBackdropImageUrl";
|
import { getParentBackdropImageUrl } from "@/utils/jellyfin/image/getParentBackdropImageUrl";
|
||||||
@@ -67,6 +68,14 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
const api = useAtomValue(apiAtom);
|
const api = useAtomValue(apiAtom);
|
||||||
const user = useAtomValue(userAtom);
|
const user = useAtomValue(userAtom);
|
||||||
|
|
||||||
|
// SyncPlay: when enabled, we DO NOT navigate locally on play — we tell the
|
||||||
|
// server, which broadcasts a PlayQueue: NewPlaylist update to every group
|
||||||
|
// member (including us). Our `setStartPlaybackHandler` in SyncPlayProvider
|
||||||
|
// then performs the navigation uniformly for everyone, matching
|
||||||
|
// jellyfin-web's playbackManager intercept (Controller.play).
|
||||||
|
const { isEnabled: isSyncPlayEnabled, controller: syncPlayController } =
|
||||||
|
useSyncPlay();
|
||||||
|
|
||||||
// Use colors prop if provided, otherwise fallback to global atom
|
// Use colors prop if provided, otherwise fallback to global atom
|
||||||
const effectiveColors = colors || globalColorAtom;
|
const effectiveColors = colors || globalColorAtom;
|
||||||
|
|
||||||
@@ -94,6 +103,37 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
const handleNormalPlayFlow = useCallback(async () => {
|
const handleNormalPlayFlow = useCallback(async () => {
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
|
|
||||||
|
// SyncPlay intercept: in a group, route playback through sthe server so
|
||||||
|
// every member gets the same PlayQueue: NewPlaylist update and navigates
|
||||||
|
// together. Skips local navigation and the Chromecast prompt entirely —
|
||||||
|
// SyncPlay + Chromecast isn't a supported combination yet, same as
|
||||||
|
// jellyfin-web.
|
||||||
|
if (isSyncPlayEnabled && syncPlayController && item.Id) {
|
||||||
|
try {
|
||||||
|
// Pass the full `item` (not just the ID) so the SyncPlay controller
|
||||||
|
// can run `translateItemsForPlayback` with full context — this is
|
||||||
|
// what jellyfin-web does, and it lets us expand Series / Season /
|
||||||
|
// BoxSet into real episode/track IDs before broadcasting the queue.
|
||||||
|
// Without expansion, receivers (jellyfin-web in particular) get
|
||||||
|
// container IDs they can't play and silently fail to open the
|
||||||
|
// player.
|
||||||
|
await syncPlayController.play({
|
||||||
|
items: [item],
|
||||||
|
ids: [item.Id],
|
||||||
|
startPositionTicks: item.UserData?.PlaybackPositionTicks ?? 0,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("SyncPlay: failed to start group playback", error);
|
||||||
|
Alert.alert(
|
||||||
|
t("player.client_error"),
|
||||||
|
t("syncplay.failed_to_start", {
|
||||||
|
defaultValue: "Failed to start SyncPlay group playback",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const queryParams = new URLSearchParams({
|
const queryParams = new URLSearchParams({
|
||||||
itemId: item.Id!,
|
itemId: item.Id!,
|
||||||
audioIndex: selectedOptions.audioIndex?.toString() ?? "",
|
audioIndex: selectedOptions.audioIndex?.toString() ?? "",
|
||||||
@@ -290,6 +330,8 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
goToPlayer,
|
goToPlayer,
|
||||||
isOffline,
|
isOffline,
|
||||||
t,
|
t,
|
||||||
|
isSyncPlayEnabled,
|
||||||
|
syncPlayController,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const onPress = useCallback(async () => {
|
const onPress = useCallback(async () => {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const TrackSheet: React.FC<Props> = ({
|
|||||||
|
|
||||||
const streams = useMemo(
|
const streams = useMemo(
|
||||||
() => source?.MediaStreams?.filter((x) => x.Type === streamType),
|
() => source?.MediaStreams?.filter((x) => x.Type === streamType),
|
||||||
[source, streamType],
|
[source],
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedSteam = useMemo(
|
const selectedSteam = useMemo(
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { FlatList, Modal, Pressable, StyleSheet, View } from "react-native";
|
import { FlatList, Modal, Pressable, StyleSheet, View } from "react-native";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import { Colors } from "@/constants/Colors";
|
import { Colors } from "@/constants/Colors";
|
||||||
|
import { useControlsSafeAreaInsets } from "@/hooks/useControlsSafeAreaInsets";
|
||||||
import {
|
import {
|
||||||
type ChapterEntry,
|
type ChapterEntry,
|
||||||
chapterStartsMs,
|
chapterStartsMs,
|
||||||
@@ -38,6 +39,7 @@ function ChapterListComponent({
|
|||||||
onClose,
|
onClose,
|
||||||
}: ChapterListProps) {
|
}: ChapterListProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const safeArea = useControlsSafeAreaInsets();
|
||||||
const listRef = useRef<FlatList<ChapterEntry>>(null);
|
const listRef = useRef<FlatList<ChapterEntry>>(null);
|
||||||
|
|
||||||
const entries = useMemo(() => sortedChapters(chapters), [chapters]);
|
const entries = useMemo(() => sortedChapters(chapters), [chapters]);
|
||||||
@@ -74,9 +76,22 @@ function ChapterListComponent({
|
|||||||
transparent
|
transparent
|
||||||
animationType='slide'
|
animationType='slide'
|
||||||
onRequestClose={onClose}
|
onRequestClose={onClose}
|
||||||
|
// iOS defaults <Modal> to portrait-only; without this it rotates the app
|
||||||
|
// back to portrait when opened from the landscape player. Android ignores it.
|
||||||
|
supportedOrientations={["portrait", "landscape"]}
|
||||||
>
|
>
|
||||||
<Pressable onPress={onClose} style={styles.backdrop}>
|
<Pressable onPress={onClose} style={styles.backdrop}>
|
||||||
<Pressable onPress={(e) => e.stopPropagation()} style={styles.sheet}>
|
<Pressable
|
||||||
|
onPress={(e) => e.stopPropagation()}
|
||||||
|
style={[
|
||||||
|
styles.sheet,
|
||||||
|
{
|
||||||
|
marginLeft: safeArea.left,
|
||||||
|
marginRight: safeArea.right,
|
||||||
|
paddingBottom: safeArea.bottom,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Text style={styles.title}>{t("chapters.title")}</Text>
|
<Text style={styles.title}>{t("chapters.title")}</Text>
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -157,14 +172,12 @@ const styles = StyleSheet.create({
|
|||||||
backdrop: {
|
backdrop: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: "flex-end",
|
justifyContent: "flex-end",
|
||||||
backgroundColor: "rgba(0,0,0,0.6)",
|
|
||||||
},
|
},
|
||||||
sheet: {
|
sheet: {
|
||||||
backgroundColor: Colors.background,
|
backgroundColor: Colors.background,
|
||||||
borderTopLeftRadius: 16,
|
borderTopLeftRadius: 16,
|
||||||
borderTopRightRadius: 16,
|
borderTopRightRadius: 16,
|
||||||
maxHeight: "70%",
|
maxHeight: "70%",
|
||||||
paddingBottom: 24,
|
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
|
|||||||
@@ -133,7 +133,6 @@ const HomeMobile = () => {
|
|||||||
onPress={() => {
|
onPress={() => {
|
||||||
router.push("/(auth)/downloads");
|
router.push("/(auth)/downloads");
|
||||||
}}
|
}}
|
||||||
className='ml-1.5'
|
|
||||||
style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
|
style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
|
||||||
>
|
>
|
||||||
<Feather
|
<Feather
|
||||||
|
|||||||
@@ -37,7 +37,20 @@ export const ItemPeopleSections: React.FC<Props> = ({ item, ...props }) => {
|
|||||||
return { ...item, People: people } as BaseItemDto;
|
return { ...item, People: people } as BaseItemDto;
|
||||||
}, [item, people]);
|
}, [item, people]);
|
||||||
|
|
||||||
const topPeople = useMemo(() => people.slice(0, 3), [people]);
|
// Jellyfin can list the same person several times (e.g. an actor also
|
||||||
|
// credited as writer). Dedupe by Id so the same actor section isn't rendered
|
||||||
|
// twice and we still surface 3 distinct people.
|
||||||
|
const topPeople = useMemo(() => {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const unique: BaseItemPerson[] = [];
|
||||||
|
for (const person of people) {
|
||||||
|
if (!person.Id || seen.has(person.Id)) continue;
|
||||||
|
seen.add(person.Id);
|
||||||
|
unique.push(person);
|
||||||
|
if (unique.length >= 3) break;
|
||||||
|
}
|
||||||
|
return unique;
|
||||||
|
}, [people]);
|
||||||
|
|
||||||
const renderActorSection = useCallback(
|
const renderActorSection = useCallback(
|
||||||
(person: BaseItemPerson, idx: number, total: number) => {
|
(person: BaseItemPerson, idx: number, total: number) => {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export const LibraryItemCard: React.FC<Props> = ({ library, ...props }) => {
|
|||||||
api,
|
api,
|
||||||
item: library,
|
item: library,
|
||||||
}),
|
}),
|
||||||
[api, library],
|
[library],
|
||||||
);
|
);
|
||||||
|
|
||||||
const itemType = useMemo(() => {
|
const itemType = useMemo(() => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import React, { useCallback, useState } from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
import { ScrollView, View } from "react-native";
|
import { Platform, ScrollView, View } from "react-native";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import { useScaledTVTypography } from "@/constants/TVTypography";
|
import { useScaledTVTypography } from "@/constants/TVTypography";
|
||||||
@@ -107,7 +107,7 @@ export const TVAddServerForm: React.FC<TVAddServerFormProps> = ({
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Pair with Phone */}
|
{/* Pair with Phone */}
|
||||||
{onStartPairing && (
|
{Platform.OS !== "ios" && onStartPairing && (
|
||||||
<View>
|
<View>
|
||||||
<Button
|
<Button
|
||||||
onPress={onStartPairing}
|
onPress={onStartPairing}
|
||||||
|
|||||||
@@ -401,10 +401,6 @@ export const TVJellyseerrSearchResults: React.FC<
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const hasMovies = movieResults && movieResults.length > 0;
|
|
||||||
const hasTv = tvResults && tvResults.length > 0;
|
|
||||||
const hasPersons = personResults && personResults.length > 0;
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -431,22 +427,26 @@ export const TVJellyseerrSearchResults: React.FC<
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
|
{/* No section requests `hasTVPreferredFocus`: the native search field
|
||||||
|
keeps focus while typing, otherwise the first result would re-grab
|
||||||
|
focus on every keystroke as results re-render. The user navigates
|
||||||
|
down to the grid manually. */}
|
||||||
<TVJellyseerrMovieSection
|
<TVJellyseerrMovieSection
|
||||||
title={t("search.request_movies")}
|
title={t("search.request_movies")}
|
||||||
items={movieResults}
|
items={movieResults}
|
||||||
isFirstSection={hasMovies}
|
isFirstSection={false}
|
||||||
onItemPress={onMoviePress}
|
onItemPress={onMoviePress}
|
||||||
/>
|
/>
|
||||||
<TVJellyseerrTvSection
|
<TVJellyseerrTvSection
|
||||||
title={t("search.request_series")}
|
title={t("search.request_series")}
|
||||||
items={tvResults}
|
items={tvResults}
|
||||||
isFirstSection={!hasMovies && hasTv}
|
isFirstSection={false}
|
||||||
onItemPress={onTvPress}
|
onItemPress={onTvPress}
|
||||||
/>
|
/>
|
||||||
<TVJellyseerrPersonSection
|
<TVJellyseerrPersonSection
|
||||||
title={t("search.actors")}
|
title={t("search.actors")}
|
||||||
items={personResults}
|
items={personResults}
|
||||||
isFirstSection={!hasMovies && !hasTv && hasPersons}
|
isFirstSection={false}
|
||||||
onItemPress={onPersonPress}
|
onItemPress={onPersonPress}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -235,10 +235,13 @@ export const TVSearchPage: React.FC<TVSearchPageProps> = ({
|
|||||||
module). It renders the native search bar + grid keyboard and
|
module). It renders the native search bar + grid keyboard and
|
||||||
forwards typed text into the existing query pipeline via setSearch;
|
forwards typed text into the existing query pipeline via setSearch;
|
||||||
our own results grid renders below. */}
|
our own results grid renders below. */}
|
||||||
|
{/* No horizontal margin here: the native tvOS search bar centers itself
|
||||||
|
and renders a trailing "Hold to Dictate in <Language>" hint. Extra
|
||||||
|
margins squeeze the bar's width and clip that trailing hint, so let
|
||||||
|
the native view span the full width and own its own insets. */}
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
marginBottom: 24,
|
marginBottom: 24,
|
||||||
marginHorizontal: HORIZONTAL_PADDING,
|
|
||||||
height: SEARCH_AREA_HEIGHT,
|
height: SEARCH_AREA_HEIGHT,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -280,13 +283,17 @@ export const TVSearchPage: React.FC<TVSearchPageProps> = ({
|
|||||||
{/* Library Search Results */}
|
{/* Library Search Results */}
|
||||||
{isLibraryMode && !loading && (
|
{isLibraryMode && !loading && (
|
||||||
<View style={{ gap: SECTION_GAP }}>
|
<View style={{ gap: SECTION_GAP }}>
|
||||||
{sections.map((section, index) => (
|
{sections.map((section) => (
|
||||||
<TVSearchSection
|
<TVSearchSection
|
||||||
key={section.key}
|
key={section.key}
|
||||||
title={section.title}
|
title={section.title}
|
||||||
items={section.items!}
|
items={section.items!}
|
||||||
orientation={section.orientation || "vertical"}
|
orientation={section.orientation || "vertical"}
|
||||||
isFirstSection={index === 0}
|
// Never auto-focus a result. The native search field owns focus
|
||||||
|
// while typing; `hasTVPreferredFocus` here would re-grab focus on
|
||||||
|
// every keystroke as results re-render. User navigates down to the
|
||||||
|
// grid manually.
|
||||||
|
isFirstSection={false}
|
||||||
onItemPress={onItemPress}
|
onItemPress={onItemPress}
|
||||||
onItemLongPress={onItemLongPress}
|
onItemLongPress={onItemLongPress}
|
||||||
imageUrlGetter={
|
imageUrlGetter={
|
||||||
|
|||||||
@@ -297,12 +297,12 @@ export const TVSearchSection: React.FC<TVSearchSectionProps> = ({
|
|||||||
removeClippedSubviews={false}
|
removeClippedSubviews={false}
|
||||||
getItemLayout={getItemLayout}
|
getItemLayout={getItemLayout}
|
||||||
style={{ overflow: "visible" }}
|
style={{ overflow: "visible" }}
|
||||||
contentInset={{
|
// Edge padding via contentContainerStyle, NOT contentInset+contentOffset.
|
||||||
left: edgePadding,
|
// contentOffset only applies on initial mount; since this FlatList is
|
||||||
right: edgePadding,
|
// reused across searches (stable key), a second search left the inset
|
||||||
}}
|
// without the offset and the grid snapped flush to the left edge.
|
||||||
contentOffset={{ x: -edgePadding, y: 0 }}
|
|
||||||
contentContainerStyle={{
|
contentContainerStyle={{
|
||||||
|
paddingHorizontal: edgePadding,
|
||||||
paddingVertical: SCALE_PADDING,
|
paddingVertical: SCALE_PADDING,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -31,8 +31,12 @@ export const SeasonEpisodesCarousel: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
const isOffline = useOfflineMode();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const isOffline = useOfflineMode();
|
||||||
|
// Read the live (cached) downloads DB inside the query rather than the
|
||||||
|
// provider's downloadedItems snapshot, so refetches after
|
||||||
|
// updateDownloadedItem() reflect the latest state instead of a stale
|
||||||
|
// refreshKey-gated snapshot. getAllDownloadedItems() is cached, so this stays cheap.
|
||||||
const { getDownloadedItems } = useDownload();
|
const { getDownloadedItems } = useDownload();
|
||||||
|
|
||||||
const scrollRef = useRef<HorizontalScrollRef>(null);
|
const scrollRef = useRef<HorizontalScrollRef>(null);
|
||||||
@@ -100,7 +104,7 @@ export const SeasonEpisodesCarousel: React.FC<Props> = ({
|
|||||||
onPress={() => {
|
onPress={() => {
|
||||||
router.setParams({ id: _item.Id });
|
router.setParams({ id: _item.Id });
|
||||||
}}
|
}}
|
||||||
className={`flex flex-col w-44
|
className={`flex flex-col w-44
|
||||||
${item?.Id === _item.Id ? "" : "opacity-50"}
|
${item?.Id === _item.Id ? "" : "opacity-50"}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ export const LibraryOptionsSheet: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
</OptionGroup>
|
</OptionGroup>
|
||||||
|
|
||||||
<OptionGroup title='Options'>
|
<OptionGroup title={t("library.options.options_title")}>
|
||||||
<ToggleItem
|
<ToggleItem
|
||||||
label={t("library.options.show_titles")}
|
label={t("library.options.show_titles")}
|
||||||
value={settings.showTitles}
|
value={settings.showTitles}
|
||||||
|
|||||||
@@ -196,7 +196,10 @@ export const OtherSettings: React.FC = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem title={t("home.settings.other.max_auto_play_episode_count")}>
|
<ListItem
|
||||||
|
title={t("home.settings.other.max_auto_play_episode_count")}
|
||||||
|
disabled={pluginSettings?.maxAutoPlayEpisodeCount?.locked}
|
||||||
|
>
|
||||||
<PlatformDropdown
|
<PlatformDropdown
|
||||||
groups={autoPlayEpisodeOptions}
|
groups={autoPlayEpisodeOptions}
|
||||||
trigger={
|
trigger={
|
||||||
|
|||||||
@@ -229,7 +229,10 @@ export const PlaybackControlsSettings: React.FC = () => {
|
|||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
title={t("home.settings.other.max_auto_play_episode_count")}
|
title={t("home.settings.other.max_auto_play_episode_count")}
|
||||||
disabled={!settings.autoPlayNextEpisode}
|
disabled={
|
||||||
|
!settings.autoPlayNextEpisode ||
|
||||||
|
pluginSettings?.maxAutoPlayEpisodeCount?.locked
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<PlatformDropdown
|
<PlatformDropdown
|
||||||
groups={autoPlayEpisodeOptions}
|
groups={autoPlayEpisodeOptions}
|
||||||
|
|||||||
235
components/syncplay/GroupSelectionMenu.tsx
Normal file
235
components/syncplay/GroupSelectionMenu.tsx
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
/**
|
||||||
|
* GroupSelectionMenu
|
||||||
|
*
|
||||||
|
* Content rendered inside the SyncPlay bottom sheet (the sheet itself is
|
||||||
|
* owned by SyncPlayButton). Calls `onClose` after successful actions to
|
||||||
|
* dismiss the parent sheet.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { ActivityIndicator, TouchableOpacity, View } from "react-native";
|
||||||
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
import { Button } from "@/components/Button";
|
||||||
|
import { Text } from "@/components/common/Text";
|
||||||
|
import { useSyncPlay } from "@/providers/SyncPlay";
|
||||||
|
import type { GroupInfoDto } from "@/providers/SyncPlay/types";
|
||||||
|
|
||||||
|
interface GroupSelectionMenuProps {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GroupSelectionMenu({ onClose }: GroupSelectionMenuProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
|
const {
|
||||||
|
isEnabled,
|
||||||
|
groupInfo,
|
||||||
|
canCreateGroups,
|
||||||
|
joinGroup,
|
||||||
|
createGroup,
|
||||||
|
leaveGroup,
|
||||||
|
getGroups,
|
||||||
|
} = useSyncPlay();
|
||||||
|
|
||||||
|
const [groups, setGroups] = useState<GroupInfoDto[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
(async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const fetchedGroups = await getGroups();
|
||||||
|
if (!cancelled) {
|
||||||
|
setGroups(fetchedGroups);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch groups", error);
|
||||||
|
} finally {
|
||||||
|
if (!cancelled) {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [getGroups]);
|
||||||
|
|
||||||
|
const handleJoinGroup = useCallback(
|
||||||
|
async (groupId: string) => {
|
||||||
|
try {
|
||||||
|
await joinGroup(groupId);
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to join group", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[joinGroup, onClose],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCreateGroup = useCallback(async () => {
|
||||||
|
setIsCreating(true);
|
||||||
|
try {
|
||||||
|
await createGroup();
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to create group", error);
|
||||||
|
} finally {
|
||||||
|
setIsCreating(false);
|
||||||
|
}
|
||||||
|
}, [createGroup, onClose]);
|
||||||
|
|
||||||
|
const handleLeaveGroup = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
await leaveGroup();
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to leave group", error);
|
||||||
|
}
|
||||||
|
}, [leaveGroup, onClose]);
|
||||||
|
|
||||||
|
const containerStyle = {
|
||||||
|
paddingLeft: Math.max(16, insets.left),
|
||||||
|
paddingRight: Math.max(16, insets.right),
|
||||||
|
paddingBottom: Math.max(16, insets.bottom),
|
||||||
|
paddingTop: 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isEnabled && groupInfo) {
|
||||||
|
return (
|
||||||
|
<View style={containerStyle}>
|
||||||
|
<View className='mb-4'>
|
||||||
|
<View className='flex-row items-center mb-2'>
|
||||||
|
<Ionicons name='people' size={24} color='#00a4dc' />
|
||||||
|
<Text className='font-bold text-xl text-neutral-100 ml-2'>
|
||||||
|
{t("syncplay.title")}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Text className='text-neutral-400'>{t("syncplay.my_group")}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='bg-neutral-800 rounded-xl p-4 mb-4'>
|
||||||
|
<View className='flex-row items-center justify-between mb-3'>
|
||||||
|
<Text className='text-neutral-100 font-semibold text-lg'>
|
||||||
|
{groupInfo.GroupName}
|
||||||
|
</Text>
|
||||||
|
<View className='bg-[#00a4dc] px-2 py-1 rounded'>
|
||||||
|
<Text className='text-white text-xs font-medium'>
|
||||||
|
{groupInfo.State}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{groupInfo.Participants && groupInfo.Participants.length > 0 && (
|
||||||
|
<View className='flex-row items-center'>
|
||||||
|
<Ionicons name='person' size={16} color='#9ca3af' />
|
||||||
|
<Text className='text-neutral-400 ml-2'>
|
||||||
|
{groupInfo.Participants.length} {t("syncplay.members")}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Button onPress={handleLeaveGroup} color='red'>
|
||||||
|
<View className='flex-row items-center justify-center'>
|
||||||
|
<Ionicons name='exit-outline' size={20} color='white' />
|
||||||
|
<Text className='text-white font-semibold ml-2'>
|
||||||
|
{t("syncplay.leave_group")}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={containerStyle}>
|
||||||
|
<View className='mb-4'>
|
||||||
|
<View className='flex-row items-center mb-2'>
|
||||||
|
<Ionicons name='people-outline' size={24} color='white' />
|
||||||
|
<Text className='font-bold text-xl text-neutral-100 ml-2'>
|
||||||
|
{t("syncplay.title")}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Text className='text-neutral-400'>{t("syncplay.join_group")}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<View className='py-8 items-center'>
|
||||||
|
<ActivityIndicator color='#00a4dc' />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && groups.length > 0 && (
|
||||||
|
<View className='mb-4'>
|
||||||
|
<Text className='text-neutral-400 text-sm mb-2 ml-1'>
|
||||||
|
{t("syncplay.available_groups")}
|
||||||
|
</Text>
|
||||||
|
<View className='bg-neutral-800 rounded-xl overflow-hidden'>
|
||||||
|
{groups.map((group, index) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={group.GroupId ?? index}
|
||||||
|
onPress={() => group.GroupId && handleJoinGroup(group.GroupId)}
|
||||||
|
className={`flex-row items-center p-4 ${
|
||||||
|
index < groups.length - 1 ? "border-b border-neutral-700" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<View className='w-10 h-10 bg-[#00a4dc]/20 rounded-full items-center justify-center mr-3'>
|
||||||
|
<Ionicons name='people' size={20} color='#00a4dc' />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='flex-1'>
|
||||||
|
<Text className='text-neutral-100 font-medium'>
|
||||||
|
{group.GroupName}
|
||||||
|
</Text>
|
||||||
|
<Text className='text-neutral-500 text-sm'>
|
||||||
|
{group.Participants?.length ?? 0} {t("syncplay.members")} •{" "}
|
||||||
|
{group.State}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Ionicons name='chevron-forward' size={20} color='#9ca3af' />
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && groups.length === 0 && (
|
||||||
|
<View className='bg-neutral-800/50 rounded-xl p-6 mb-4 items-center'>
|
||||||
|
<Ionicons name='people-outline' size={40} color='#6b7280' />
|
||||||
|
<Text className='text-neutral-400 text-center mt-3'>
|
||||||
|
{t("syncplay.available_groups")}: 0{"\n"}
|
||||||
|
{t("syncplay.create_new_group")}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{canCreateGroups && (
|
||||||
|
<Button
|
||||||
|
onPress={handleCreateGroup}
|
||||||
|
color='purple'
|
||||||
|
disabled={isCreating}
|
||||||
|
>
|
||||||
|
<View className='flex-row items-center justify-center'>
|
||||||
|
{isCreating ? (
|
||||||
|
<ActivityIndicator size='small' color='white' />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Ionicons name='add' size={20} color='white' />
|
||||||
|
<Text className='text-white font-semibold ml-2'>
|
||||||
|
{t("syncplay.create_new_group")}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
206
components/syncplay/SyncPlayActionIcon.tsx
Normal file
206
components/syncplay/SyncPlayActionIcon.tsx
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
/**
|
||||||
|
* SyncPlayActionIcon
|
||||||
|
*
|
||||||
|
* In-button SyncPlay status indicator — drops into the player's
|
||||||
|
* play/pause button slot and replaces the normal play/pause/loader
|
||||||
|
* graphic while SyncPlay is mid-transition. Mirrors jellyfin-web's
|
||||||
|
* `#syncPlayIcon` element (see `showIcon()` in
|
||||||
|
* `jellyfin-web/src/controllers/playback/video/index.js`).
|
||||||
|
*
|
||||||
|
* Icon vocabulary (matched 1:1 with jellyfin-web's `showIcon` switch):
|
||||||
|
*
|
||||||
|
* action primary secondary pulse spin
|
||||||
|
* --------------- ------------- ----------------- ---------- ----
|
||||||
|
* schedule-play sync play (centered) infinite yes
|
||||||
|
* unpause play-circle — one-shot no
|
||||||
|
* pause pause-circle — one-shot no
|
||||||
|
* seek refresh — infinite no
|
||||||
|
* buffering clock — infinite no
|
||||||
|
* wait-pause clock pause (shifted) infinite no
|
||||||
|
* wait-unpause clock play (shifted) infinite no
|
||||||
|
*
|
||||||
|
* Material → Ionicons mapping used here:
|
||||||
|
* sync → sync, schedule → time-outline, update → refresh-outline,
|
||||||
|
* play_arrow → play, pause → pause,
|
||||||
|
* play_circle_outline → play-circle-outline,
|
||||||
|
* pause_circle_outline → pause-circle-outline.
|
||||||
|
*
|
||||||
|
* When no SyncPlay action is active the component renders `fallback`
|
||||||
|
* so callers can keep the normal play/pause/loader graphic.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { type ReactNode, useEffect } from "react";
|
||||||
|
import { StyleSheet, View } from "react-native";
|
||||||
|
import Animated, {
|
||||||
|
cancelAnimation,
|
||||||
|
Easing,
|
||||||
|
useAnimatedStyle,
|
||||||
|
useSharedValue,
|
||||||
|
withRepeat,
|
||||||
|
withSequence,
|
||||||
|
withTiming,
|
||||||
|
} from "react-native-reanimated";
|
||||||
|
import type { SyncPlayOsdAction } from "@/providers/SyncPlay";
|
||||||
|
import { useSyncPlay } from "@/providers/SyncPlay";
|
||||||
|
|
||||||
|
// SyncPlay cyan (matches jellyfin-web's `.syncPlayIconCircle` color)
|
||||||
|
const SYNC_PLAY_COLOR = "#00a4dc";
|
||||||
|
|
||||||
|
type IoniconName = keyof typeof Ionicons.glyphMap;
|
||||||
|
|
||||||
|
type SecondaryPosition = "centered" | "shifted";
|
||||||
|
|
||||||
|
interface SecondaryIcon {
|
||||||
|
icon: IoniconName;
|
||||||
|
position: SecondaryPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OsdConfig {
|
||||||
|
/** Primary icon — fills the available size. */
|
||||||
|
icon: IoniconName;
|
||||||
|
/** Optional smaller overlay (~42% size). */
|
||||||
|
secondary?: SecondaryIcon;
|
||||||
|
/** Wrapper-level scale animation. */
|
||||||
|
pulse: "infinite" | "oneshot";
|
||||||
|
/** Rotate the primary icon continuously (secondary stays still). */
|
||||||
|
spin?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CONFIG: Record<SyncPlayOsdAction, OsdConfig> = {
|
||||||
|
"schedule-play": {
|
||||||
|
icon: "sync",
|
||||||
|
secondary: { icon: "play", position: "centered" },
|
||||||
|
pulse: "infinite",
|
||||||
|
spin: true,
|
||||||
|
},
|
||||||
|
unpause: { icon: "play-circle-outline", pulse: "oneshot" },
|
||||||
|
pause: { icon: "pause-circle-outline", pulse: "oneshot" },
|
||||||
|
seek: { icon: "refresh-outline", pulse: "infinite" },
|
||||||
|
buffering: { icon: "time-outline", pulse: "infinite" },
|
||||||
|
"wait-pause": {
|
||||||
|
icon: "time-outline",
|
||||||
|
secondary: { icon: "pause", position: "shifted" },
|
||||||
|
pulse: "infinite",
|
||||||
|
},
|
||||||
|
"wait-unpause": {
|
||||||
|
icon: "time-outline",
|
||||||
|
secondary: { icon: "play", position: "shifted" },
|
||||||
|
pulse: "infinite",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SyncPlayActionIconProps {
|
||||||
|
size: number;
|
||||||
|
color?: string;
|
||||||
|
/** Rendered when no SyncPlay action is active. */
|
||||||
|
fallback?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SyncPlayActionIcon({
|
||||||
|
size,
|
||||||
|
color = SYNC_PLAY_COLOR,
|
||||||
|
fallback = null,
|
||||||
|
}: SyncPlayActionIconProps) {
|
||||||
|
const { osdAction } = useSyncPlay();
|
||||||
|
|
||||||
|
const rotation = useSharedValue(0);
|
||||||
|
const scale = useSharedValue(1);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
cancelAnimation(rotation);
|
||||||
|
cancelAnimation(scale);
|
||||||
|
rotation.value = 0;
|
||||||
|
scale.value = 1;
|
||||||
|
|
||||||
|
if (!osdAction) return;
|
||||||
|
|
||||||
|
const config = CONFIG[osdAction];
|
||||||
|
|
||||||
|
if (config.spin) {
|
||||||
|
rotation.value = withRepeat(
|
||||||
|
withTiming(360, { duration: 1200, easing: Easing.linear }),
|
||||||
|
-1,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.pulse === "infinite") {
|
||||||
|
scale.value = withRepeat(
|
||||||
|
withSequence(
|
||||||
|
withTiming(1.1, {
|
||||||
|
duration: 700,
|
||||||
|
easing: Easing.inOut(Easing.quad),
|
||||||
|
}),
|
||||||
|
withTiming(0.95, {
|
||||||
|
duration: 700,
|
||||||
|
easing: Easing.inOut(Easing.quad),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
-1,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// one-shot: single scale flash; the provider clears the action
|
||||||
|
// ~1500ms later (transient OSD) so the icon then unmounts.
|
||||||
|
scale.value = withSequence(
|
||||||
|
withTiming(1.2, { duration: 220, easing: Easing.out(Easing.quad) }),
|
||||||
|
withTiming(1, { duration: 220, easing: Easing.inOut(Easing.quad) }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [osdAction, rotation, scale]);
|
||||||
|
|
||||||
|
const pulseStyle = useAnimatedStyle(() => ({
|
||||||
|
transform: [{ scale: scale.value }],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const spinStyle = useAnimatedStyle(() => ({
|
||||||
|
transform: [{ rotate: `${rotation.value}deg` }],
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!osdAction) return <>{fallback}</>;
|
||||||
|
|
||||||
|
const config = CONFIG[osdAction];
|
||||||
|
const secondarySize = Math.round(size * 0.42);
|
||||||
|
|
||||||
|
// centered: geometric middle of the primary (e.g. play arrow inside
|
||||||
|
// the spinning `sync` ring for schedule-play).
|
||||||
|
// shifted: bottom-right corner (e.g. play/pause badge on the clock
|
||||||
|
// for wait-unpause / wait-pause).
|
||||||
|
const secondaryPosStyle =
|
||||||
|
config.secondary?.position === "centered"
|
||||||
|
? {
|
||||||
|
top: (size - secondarySize) / 2,
|
||||||
|
left: (size - secondarySize) / 2,
|
||||||
|
}
|
||||||
|
: { bottom: 0, right: 0 };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Animated.View style={pulseStyle}>
|
||||||
|
<View style={{ width: size, height: size }}>
|
||||||
|
<Animated.View style={[StyleSheet.absoluteFill, spinStyle]}>
|
||||||
|
<Ionicons name={config.icon} size={size} color={color} />
|
||||||
|
</Animated.View>
|
||||||
|
|
||||||
|
{config.secondary && (
|
||||||
|
<View
|
||||||
|
pointerEvents='none'
|
||||||
|
style={[styles.secondary, secondaryPosStyle]}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name={config.secondary.icon}
|
||||||
|
size={secondarySize}
|
||||||
|
color={color}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</Animated.View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
secondary: {
|
||||||
|
position: "absolute",
|
||||||
|
},
|
||||||
|
});
|
||||||
97
components/syncplay/SyncPlayButton.tsx
Normal file
97
components/syncplay/SyncPlayButton.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* SyncPlayButton
|
||||||
|
*
|
||||||
|
* Header button for accessing SyncPlay functionality.
|
||||||
|
* Shows group status and opens the group selection sheet.
|
||||||
|
*
|
||||||
|
* Uses the @expo/ui drop-in BottomSheetModal (SwiftUI sheet on iOS, Jetpack
|
||||||
|
* Compose ModalBottomSheet on Android). Because it presents natively, it
|
||||||
|
* works correctly even when triggered from `headerRight` — no portal or
|
||||||
|
* provider context is required (unlike @gorhom/bottom-sheet, which fails
|
||||||
|
* silently from detached UINavigationItem subtrees).
|
||||||
|
*
|
||||||
|
* Safe to import statically: this whole module is lazy-required only on
|
||||||
|
* non-TV platforms by app/(auth)/(tabs)/(home)/_layout.tsx.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
type BottomSheetMethods,
|
||||||
|
BottomSheetModal,
|
||||||
|
BottomSheetView,
|
||||||
|
} from "@expo/ui/community/bottom-sheet";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { useCallback, useRef } from "react";
|
||||||
|
import { Platform, View } from "react-native";
|
||||||
|
import { Pressable } from "react-native-gesture-handler";
|
||||||
|
import { useCastDevice } from "react-native-google-cast";
|
||||||
|
import { toast } from "sonner-native";
|
||||||
|
import { useNetworkStatus } from "@/providers/NetworkStatusProvider";
|
||||||
|
import { useSyncPlay } from "@/providers/SyncPlay";
|
||||||
|
import { GroupSelectionMenu } from "./GroupSelectionMenu";
|
||||||
|
|
||||||
|
interface SyncPlayButtonProps {
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SyncPlayButton({ size = 22 }: SyncPlayButtonProps) {
|
||||||
|
const { isEnabled, canJoinGroups } = useSyncPlay();
|
||||||
|
const { isConnected } = useNetworkStatus();
|
||||||
|
const castDevice = useCastDevice();
|
||||||
|
const sheetRef = useRef<BottomSheetMethods>(null);
|
||||||
|
|
||||||
|
const isCasting = !!castDevice;
|
||||||
|
|
||||||
|
const handlePress = useCallback(() => {
|
||||||
|
if (isCasting) {
|
||||||
|
toast("SyncPlay not available while casting");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sheetRef.current?.present();
|
||||||
|
}, [isCasting]);
|
||||||
|
|
||||||
|
const handleDismiss = useCallback(() => {
|
||||||
|
sheetRef.current?.dismiss();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (Platform.isTV) return null;
|
||||||
|
if (!canJoinGroups) return null;
|
||||||
|
if (!isConnected) return null;
|
||||||
|
|
||||||
|
const iconColor = isCasting ? "#6b7280" : isEnabled ? "#00a4dc" : "white";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Pressable
|
||||||
|
className='mr-4'
|
||||||
|
onPress={handlePress}
|
||||||
|
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||||
|
>
|
||||||
|
<View className='relative'>
|
||||||
|
<Ionicons
|
||||||
|
name={isEnabled ? "people" : "people-outline"}
|
||||||
|
size={size}
|
||||||
|
color={iconColor}
|
||||||
|
/>
|
||||||
|
{isEnabled && !isCasting && (
|
||||||
|
<View
|
||||||
|
className='absolute -top-0.5 -right-0.5 w-2.5 h-2.5 rounded-full bg-[#00a4dc]'
|
||||||
|
style={{
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#171717",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
<BottomSheetModal
|
||||||
|
ref={sheetRef}
|
||||||
|
snapPoints={Platform.OS === "android" ? ["100%"] : ["60%"]}
|
||||||
|
enablePanDownToClose
|
||||||
|
>
|
||||||
|
<BottomSheetView>
|
||||||
|
<GroupSelectionMenu onClose={handleDismiss} />
|
||||||
|
</BottomSheetView>
|
||||||
|
</BottomSheetModal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
53
components/syncplay/SyncPlaySpinner.tsx
Normal file
53
components/syncplay/SyncPlaySpinner.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* SyncPlaySpinner
|
||||||
|
*
|
||||||
|
* Compact rotating SyncPlay icon shown in place of the play/pause button
|
||||||
|
* while a play/pause command is in flight to the server (the "schedule-play"
|
||||||
|
* indicator from jellyfin-web).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import Animated, {
|
||||||
|
Easing,
|
||||||
|
useAnimatedStyle,
|
||||||
|
useSharedValue,
|
||||||
|
withRepeat,
|
||||||
|
withTiming,
|
||||||
|
} from "react-native-reanimated";
|
||||||
|
|
||||||
|
// SyncPlay cyan color (matches jellyfin-web)
|
||||||
|
const SYNC_PLAY_COLOR = "#00a4dc";
|
||||||
|
|
||||||
|
interface SyncPlaySpinnerProps {
|
||||||
|
size: number;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SyncPlaySpinner({
|
||||||
|
size,
|
||||||
|
color = SYNC_PLAY_COLOR,
|
||||||
|
}: SyncPlaySpinnerProps) {
|
||||||
|
const rotation = useSharedValue(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
rotation.value = withRepeat(
|
||||||
|
withTiming(360, {
|
||||||
|
duration: 1200,
|
||||||
|
easing: Easing.linear,
|
||||||
|
}),
|
||||||
|
-1,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}, [rotation]);
|
||||||
|
|
||||||
|
const animatedStyle = useAnimatedStyle(() => ({
|
||||||
|
transform: [{ rotate: `${rotation.value}deg` }],
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Animated.View style={animatedStyle}>
|
||||||
|
<Ionicons name='sync' size={size} color={color} />
|
||||||
|
</Animated.View>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
components/syncplay/index.ts
Normal file
8
components/syncplay/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* SyncPlay UI Components
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { GroupSelectionMenu } from "./GroupSelectionMenu";
|
||||||
|
export { SyncPlayActionIcon } from "./SyncPlayActionIcon";
|
||||||
|
export { SyncPlayButton } from "./SyncPlayButton";
|
||||||
|
export { SyncPlaySpinner } from "./SyncPlaySpinner";
|
||||||
@@ -8,10 +8,10 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Pressable, View } from "react-native";
|
import { Pressable, View } from "react-native";
|
||||||
import { Slider } from "react-native-awesome-slider";
|
import { Slider } from "react-native-awesome-slider";
|
||||||
import { type SharedValue } from "react-native-reanimated";
|
import { type SharedValue } from "react-native-reanimated";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
||||||
import { ChapterList } from "@/components/chapters/ChapterList";
|
import { ChapterList } from "@/components/chapters/ChapterList";
|
||||||
import { ChapterTicks } from "@/components/chapters/ChapterTicks";
|
import { ChapterTicks } from "@/components/chapters/ChapterTicks";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
|
import { useControlsSafeAreaInsets } from "@/hooks/useControlsSafeAreaInsets";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { chapterMarkers, chapterNameAt } from "@/utils/chapters";
|
import { chapterMarkers, chapterNameAt } from "@/utils/chapters";
|
||||||
import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton";
|
import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton";
|
||||||
@@ -75,9 +75,6 @@ interface BottomControlsProps {
|
|||||||
minutes: number;
|
minutes: number;
|
||||||
seconds: number;
|
seconds: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Chapter props
|
|
||||||
chapterPositions?: number[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BottomControls: FC<BottomControlsProps> = ({
|
export const BottomControls: FC<BottomControlsProps> = ({
|
||||||
@@ -111,11 +108,10 @@ export const BottomControls: FC<BottomControlsProps> = ({
|
|||||||
trickPlayUrl,
|
trickPlayUrl,
|
||||||
trickplayInfo,
|
trickplayInfo,
|
||||||
time,
|
time,
|
||||||
chapterPositions = [],
|
|
||||||
}) => {
|
}) => {
|
||||||
const { settings } = useSettings();
|
const { settings } = useSettings();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useControlsSafeAreaInsets();
|
||||||
const [chapterListVisible, setChapterListVisible] = useState(false);
|
const [chapterListVisible, setChapterListVisible] = useState(false);
|
||||||
|
|
||||||
// Only expose chapter UI when there are at least two real markers.
|
// Only expose chapter UI when there are at least two real markers.
|
||||||
@@ -146,13 +142,9 @@ export const BottomControls: FC<BottomControlsProps> = ({
|
|||||||
style={[
|
style={[
|
||||||
{
|
{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
right:
|
right: insets.right,
|
||||||
(settings?.safeAreaInControlsEnabled ?? true) ? insets.right : 0,
|
left: insets.left,
|
||||||
left: (settings?.safeAreaInControlsEnabled ?? true) ? insets.left : 0,
|
bottom: Math.max(insets.bottom - 17, 0),
|
||||||
bottom:
|
|
||||||
(settings?.safeAreaInControlsEnabled ?? true)
|
|
||||||
? Math.max(insets.bottom - 17, 0)
|
|
||||||
: 0,
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
className={"flex flex-col px-2"}
|
className={"flex flex-col px-2"}
|
||||||
@@ -188,17 +180,6 @@ export const BottomControls: FC<BottomControlsProps> = ({
|
|||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
<View className='flex flex-row items-center space-x-2 shrink-0'>
|
<View className='flex flex-row items-center space-x-2 shrink-0'>
|
||||||
{hasChapters && (
|
|
||||||
<Pressable
|
|
||||||
onPress={() => setChapterListVisible(true)}
|
|
||||||
hitSlop={10}
|
|
||||||
className='justify-center mr-4'
|
|
||||||
accessibilityRole='button'
|
|
||||||
accessibilityLabel={t("chapters.open")}
|
|
||||||
>
|
|
||||||
<Ionicons name='bookmarks' size={24} color='white' />
|
|
||||||
</Pressable>
|
|
||||||
)}
|
|
||||||
<SkipButton
|
<SkipButton
|
||||||
showButton={showSkipButton}
|
showButton={showSkipButton}
|
||||||
onPress={skipIntro}
|
onPress={skipIntro}
|
||||||
@@ -230,6 +211,17 @@ export const BottomControls: FC<BottomControlsProps> = ({
|
|||||||
onPress={handleNextEpisodeManual}
|
onPress={handleNextEpisodeManual}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{hasChapters && (
|
||||||
|
<Pressable
|
||||||
|
onPress={() => setChapterListVisible(true)}
|
||||||
|
hitSlop={10}
|
||||||
|
className='justify-center ml-4'
|
||||||
|
accessibilityRole='button'
|
||||||
|
accessibilityLabel={t("chapters.open")}
|
||||||
|
>
|
||||||
|
<Ionicons name='bookmarks' size={24} color='white' />
|
||||||
|
</Pressable>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View
|
<View
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { Platform, TouchableOpacity, View } from "react-native";
|
import { Platform, TouchableOpacity, View } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import { Loader } from "@/components/Loader";
|
import { Loader } from "@/components/Loader";
|
||||||
|
import { SyncPlayActionIcon } from "@/components/syncplay/SyncPlayActionIcon";
|
||||||
|
import { useControlsSafeAreaInsets } from "@/hooks/useControlsSafeAreaInsets";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import AudioSlider from "./AudioSlider";
|
import AudioSlider from "./AudioSlider";
|
||||||
import BrightnessSlider from "./BrightnessSlider";
|
import BrightnessSlider from "./BrightnessSlider";
|
||||||
@@ -42,15 +43,15 @@ export const CenterControls: FC<CenterControlsProps> = ({
|
|||||||
goToNextChapter,
|
goToNextChapter,
|
||||||
}) => {
|
}) => {
|
||||||
const { settings } = useSettings();
|
const { settings } = useSettings();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useControlsSafeAreaInsets();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: "50%",
|
top: "50%",
|
||||||
left: (settings?.safeAreaInControlsEnabled ?? true) ? insets.left : 0,
|
left: insets.left,
|
||||||
right: (settings?.safeAreaInControlsEnabled ?? true) ? insets.right : 0,
|
right: insets.right,
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@@ -121,15 +122,20 @@ export const CenterControls: FC<CenterControlsProps> = ({
|
|||||||
|
|
||||||
<View style={Platform.isTV ? { flex: 1, alignItems: "center" } : {}}>
|
<View style={Platform.isTV ? { flex: 1, alignItems: "center" } : {}}>
|
||||||
<TouchableOpacity onPress={togglePlay}>
|
<TouchableOpacity onPress={togglePlay}>
|
||||||
{!isBuffering ? (
|
<SyncPlayActionIcon
|
||||||
<Ionicons
|
size={ICON_SIZES.CENTER}
|
||||||
name={isPlaying ? "pause" : "play"}
|
fallback={
|
||||||
size={ICON_SIZES.CENTER}
|
!isBuffering ? (
|
||||||
color='white'
|
<Ionicons
|
||||||
/>
|
name={isPlaying ? "pause" : "play"}
|
||||||
) : (
|
size={ICON_SIZES.CENTER}
|
||||||
<Loader size={"large"} />
|
color='white'
|
||||||
)}
|
/>
|
||||||
|
) : (
|
||||||
|
<Loader size={"large"} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@@ -219,7 +219,6 @@ export const Controls: FC<Props> = ({
|
|||||||
hasNextChapter,
|
hasNextChapter,
|
||||||
goToPreviousChapter,
|
goToPreviousChapter,
|
||||||
goToNextChapter,
|
goToNextChapter,
|
||||||
chapterPositions,
|
|
||||||
} = useChapterNavigation({
|
} = useChapterNavigation({
|
||||||
chapters: item.Chapters,
|
chapters: item.Chapters,
|
||||||
progress,
|
progress,
|
||||||
@@ -366,7 +365,9 @@ export const Controls: FC<Props> = ({
|
|||||||
{ applyLanguagePreferences: true },
|
{ applyLanguagePreferences: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
const queryParams = new URLSearchParams({
|
// Use setParams instead of replace to avoid unmounting/remounting the player,
|
||||||
|
// which would create a new MPV native view and crash with "mp_initialize already initialized".
|
||||||
|
router.setParams({
|
||||||
...(offline && { offline: "true" }),
|
...(offline && { offline: "true" }),
|
||||||
itemId: item.Id ?? "",
|
itemId: item.Id ?? "",
|
||||||
audioIndex: defaultAudioIndex?.toString() ?? "",
|
audioIndex: defaultAudioIndex?.toString() ?? "",
|
||||||
@@ -375,11 +376,17 @@ export const Controls: FC<Props> = ({
|
|||||||
bitrateValue: bitrateValue?.toString(),
|
bitrateValue: bitrateValue?.toString(),
|
||||||
playbackPosition:
|
playbackPosition:
|
||||||
item.UserData?.PlaybackPositionTicks?.toString() ?? "",
|
item.UserData?.PlaybackPositionTicks?.toString() ?? "",
|
||||||
}).toString();
|
});
|
||||||
|
|
||||||
router.replace(`player/direct-player?${queryParams}` as any);
|
|
||||||
},
|
},
|
||||||
[settings, subtitleIndex, audioIndex, mediaSource, bitrateValue, router],
|
[
|
||||||
|
settings,
|
||||||
|
subtitleIndex,
|
||||||
|
audioIndex,
|
||||||
|
mediaSource,
|
||||||
|
bitrateValue,
|
||||||
|
router,
|
||||||
|
offline,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const goToPreviousItem = useCallback(() => {
|
const goToPreviousItem = useCallback(() => {
|
||||||
@@ -585,7 +592,6 @@ export const Controls: FC<Props> = ({
|
|||||||
trickPlayUrl={trickPlayUrl}
|
trickPlayUrl={trickPlayUrl}
|
||||||
trickplayInfo={trickplayInfo}
|
trickplayInfo={trickplayInfo}
|
||||||
time={isSliding || showRemoteBubble ? time : remoteTime}
|
time={isSliding || showRemoteBubble ? time : remoteTime}
|
||||||
chapterPositions={chapterPositions}
|
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1254,7 +1254,7 @@ export const Controls: FC<Props> = ({
|
|||||||
<Text
|
<Text
|
||||||
style={[styles.endsAtText, { fontSize: typography.callout }]}
|
style={[styles.endsAtText, { fontSize: typography.callout }]}
|
||||||
>
|
>
|
||||||
{t("player.ends_at")} {getFinishTime()}
|
{t("player.ends_at", { time: getFinishTime() })}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@@ -1448,7 +1448,7 @@ export const Controls: FC<Props> = ({
|
|||||||
<Text
|
<Text
|
||||||
style={[styles.endsAtText, { fontSize: typography.callout }]}
|
style={[styles.endsAtText, { fontSize: typography.callout }]}
|
||||||
>
|
>
|
||||||
{t("player.ends_at")} {getFinishTime()}
|
{t("player.ends_at", { time: getFinishTime() })}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|||||||
import { atom, useAtom } from "jotai";
|
import { atom, useAtom } from "jotai";
|
||||||
import { useEffect, useMemo, useRef } from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
import { TouchableOpacity, View } from "react-native";
|
import { TouchableOpacity, View } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
||||||
import ContinueWatchingPoster from "@/components/ContinueWatchingPoster";
|
import ContinueWatchingPoster from "@/components/ContinueWatchingPoster";
|
||||||
import {
|
import {
|
||||||
HorizontalScroll,
|
HorizontalScroll,
|
||||||
@@ -17,10 +16,10 @@ import {
|
|||||||
SeasonDropdown,
|
SeasonDropdown,
|
||||||
type SeasonIndexState,
|
type SeasonIndexState,
|
||||||
} from "@/components/series/SeasonDropdown";
|
} from "@/components/series/SeasonDropdown";
|
||||||
|
import { useControlsSafeAreaInsets } from "@/hooks/useControlsSafeAreaInsets";
|
||||||
import { useDownload } from "@/providers/DownloadProvider";
|
import { useDownload } from "@/providers/DownloadProvider";
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
import { useOfflineMode } from "@/providers/OfflineModeProvider";
|
import { useOfflineMode } from "@/providers/OfflineModeProvider";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
|
||||||
import {
|
import {
|
||||||
getDownloadedEpisodesForSeason,
|
getDownloadedEpisodesForSeason,
|
||||||
getDownloadedSeasonNumbers,
|
getDownloadedSeasonNumbers,
|
||||||
@@ -46,8 +45,7 @@ export const EpisodeList: React.FC<Props> = ({ item, close, goToItem }) => {
|
|||||||
scrollViewRef.current?.scrollToIndex(index, 100);
|
scrollViewRef.current?.scrollToIndex(index, 100);
|
||||||
};
|
};
|
||||||
const isOffline = useOfflineMode();
|
const isOffline = useOfflineMode();
|
||||||
const { settings } = useSettings();
|
const insets = useControlsSafeAreaInsets();
|
||||||
const insets = useSafeAreaInsets();
|
|
||||||
|
|
||||||
// Set the initial season index
|
// Set the initial season index
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -59,6 +57,11 @@ export const EpisodeList: React.FC<Props> = ({ item, close, goToItem }) => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Read the live (cached) downloads DB inside the query rather than the
|
||||||
|
// provider's downloadedItems snapshot. The snapshot only refreshes on the
|
||||||
|
// provider refreshKey, so after updateDownloadedItem() invalidates
|
||||||
|
// ["episodes"]/["seasons"] (e.g. progress/played writes) the refetch would
|
||||||
|
// return stale data. getAllDownloadedItems() is cached, so this stays cheap.
|
||||||
const { getDownloadedItems } = useDownload();
|
const { getDownloadedItems } = useDownload();
|
||||||
|
|
||||||
const seasonIndex = seasonIndexState[item.ParentId ?? ""];
|
const seasonIndex = seasonIndexState[item.ParentId ?? ""];
|
||||||
@@ -182,12 +185,9 @@ export const EpisodeList: React.FC<Props> = ({ item, close, goToItem }) => {
|
|||||||
backgroundColor: "black",
|
backgroundColor: "black",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
paddingTop:
|
paddingTop: insets.top,
|
||||||
(settings?.safeAreaInControlsEnabled ?? true) ? insets.top : 0,
|
paddingLeft: insets.left,
|
||||||
paddingLeft:
|
paddingRight: insets.right,
|
||||||
(settings?.safeAreaInControlsEnabled ?? true) ? insets.left : 0,
|
|
||||||
paddingRight:
|
|
||||||
(settings?.safeAreaInControlsEnabled ?? true) ? insets.right : 0,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
|
|||||||
@@ -5,12 +5,11 @@ import type {
|
|||||||
} from "@jellyfin/sdk/lib/generated-client";
|
} from "@jellyfin/sdk/lib/generated-client";
|
||||||
import { type FC, useCallback, useState } from "react";
|
import { type FC, useCallback, useState } from "react";
|
||||||
import { Platform, TouchableOpacity, View } from "react-native";
|
import { Platform, TouchableOpacity, View } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
||||||
import useRouter from "@/hooks/useAppRouter";
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
|
import { useControlsSafeAreaInsets } from "@/hooks/useControlsSafeAreaInsets";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
import { useOrientation } from "@/hooks/useOrientation";
|
import { useOrientation } from "@/hooks/useOrientation";
|
||||||
import { OrientationLock } from "@/packages/expo-screen-orientation";
|
import { OrientationLock } from "@/packages/expo-screen-orientation";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
|
||||||
import { HEADER_LAYOUT, ICON_SIZES } from "./constants";
|
import { HEADER_LAYOUT, ICON_SIZES } from "./constants";
|
||||||
import DropdownView from "./dropdown/DropdownView";
|
import DropdownView from "./dropdown/DropdownView";
|
||||||
import { PlaybackSpeedScope } from "./utils/playback-speed-settings";
|
import { PlaybackSpeedScope } from "./utils/playback-speed-settings";
|
||||||
@@ -58,9 +57,8 @@ export const HeaderControls: FC<HeaderControlsProps> = ({
|
|||||||
showTechnicalInfo = false,
|
showTechnicalInfo = false,
|
||||||
onToggleTechnicalInfo,
|
onToggleTechnicalInfo,
|
||||||
}) => {
|
}) => {
|
||||||
const { settings } = useSettings();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useControlsSafeAreaInsets();
|
||||||
const lightHapticFeedback = useHaptic("light");
|
const lightHapticFeedback = useHaptic("light");
|
||||||
const { orientation, lockOrientation } = useOrientation();
|
const { orientation, lockOrientation } = useOrientation();
|
||||||
const [isTogglingOrientation, setIsTogglingOrientation] = useState(false);
|
const [isTogglingOrientation, setIsTogglingOrientation] = useState(false);
|
||||||
@@ -99,10 +97,9 @@ export const HeaderControls: FC<HeaderControlsProps> = ({
|
|||||||
style={[
|
style={[
|
||||||
{
|
{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: (settings?.safeAreaInControlsEnabled ?? true) ? insets.top : 0,
|
top: insets.top,
|
||||||
left: (settings?.safeAreaInControlsEnabled ?? true) ? insets.left : 0,
|
left: insets.left,
|
||||||
right:
|
right: insets.right,
|
||||||
(settings?.safeAreaInControlsEnabled ?? true) ? insets.right : 0,
|
|
||||||
padding: HEADER_LAYOUT.CONTAINER_PADDING,
|
padding: HEADER_LAYOUT.CONTAINER_PADDING,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import Animated, {
|
|||||||
} from "react-native-reanimated";
|
} from "react-native-reanimated";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { useScaledTVTypography } from "@/constants/TVTypography";
|
import { useScaledTVTypography } from "@/constants/TVTypography";
|
||||||
|
import { useControlsSafeAreaInsets } from "@/hooks/useControlsSafeAreaInsets";
|
||||||
import type { TechnicalInfo } from "@/modules/mpv-player";
|
import type { TechnicalInfo } from "@/modules/mpv-player";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
|
||||||
import { HEADER_LAYOUT } from "./constants";
|
import { HEADER_LAYOUT } from "./constants";
|
||||||
|
|
||||||
type PlayMethod = "DirectPlay" | "DirectStream" | "Transcode";
|
type PlayMethod = "DirectPlay" | "DirectStream" | "Transcode";
|
||||||
@@ -184,8 +184,8 @@ export const TechnicalInfoOverlay: FC<TechnicalInfoOverlayProps> = memo(
|
|||||||
currentAudioIndex,
|
currentAudioIndex,
|
||||||
}) => {
|
}) => {
|
||||||
const typography = useScaledTVTypography();
|
const typography = useScaledTVTypography();
|
||||||
const { settings } = useSettings();
|
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
const safeInsets = useControlsSafeAreaInsets();
|
||||||
const [info, setInfo] = useState<TechnicalInfo | null>(null);
|
const [info, setInfo] = useState<TechnicalInfo | null>(null);
|
||||||
|
|
||||||
const opacity = useSharedValue(0);
|
const opacity = useSharedValue(0);
|
||||||
@@ -268,14 +268,8 @@ export const TechnicalInfoOverlay: FC<TechnicalInfoOverlayProps> = memo(
|
|||||||
left: Math.max(insets.left, 48) + 20,
|
left: Math.max(insets.left, 48) + 20,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
top:
|
top: safeInsets.top + HEADER_LAYOUT.CONTAINER_PADDING + 4,
|
||||||
(settings?.safeAreaInControlsEnabled ?? true)
|
left: safeInsets.left + HEADER_LAYOUT.CONTAINER_PADDING + 20,
|
||||||
? insets.top + HEADER_LAYOUT.CONTAINER_PADDING + 4
|
|
||||||
: HEADER_LAYOUT.CONTAINER_PADDING + 4,
|
|
||||||
left:
|
|
||||||
(settings?.safeAreaInControlsEnabled ?? true)
|
|
||||||
? insets.left + HEADER_LAYOUT.CONTAINER_PADDING + 20
|
|
||||||
: HEADER_LAYOUT.CONTAINER_PADDING + 20,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const textStyle = Platform.isTV
|
const textStyle = Platform.isTV
|
||||||
|
|||||||
15
eas.json
15
eas.json
@@ -56,7 +56,11 @@
|
|||||||
"environment": "production",
|
"environment": "production",
|
||||||
"autoIncrement": true,
|
"autoIncrement": true,
|
||||||
"android": {
|
"android": {
|
||||||
"image": "latest"
|
"image": "latest",
|
||||||
|
"config": "android-production.yml"
|
||||||
|
},
|
||||||
|
"ios": {
|
||||||
|
"config": "ios-production.yml"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"production-apk": {
|
"production-apk": {
|
||||||
@@ -65,7 +69,8 @@
|
|||||||
"autoIncrement": true,
|
"autoIncrement": true,
|
||||||
"android": {
|
"android": {
|
||||||
"buildType": "apk",
|
"buildType": "apk",
|
||||||
"image": "latest"
|
"image": "latest",
|
||||||
|
"config": "android-production-apk.yml"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"production-apk-tv": {
|
"production-apk-tv": {
|
||||||
@@ -74,7 +79,8 @@
|
|||||||
"autoIncrement": true,
|
"autoIncrement": true,
|
||||||
"android": {
|
"android": {
|
||||||
"buildType": "apk",
|
"buildType": "apk",
|
||||||
"image": "latest"
|
"image": "latest",
|
||||||
|
"config": "android-production-tv.yml"
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"EXPO_TV": "1"
|
"EXPO_TV": "1"
|
||||||
@@ -88,7 +94,8 @@
|
|||||||
"EXPO_TV": "1"
|
"EXPO_TV": "1"
|
||||||
},
|
},
|
||||||
"ios": {
|
"ios": {
|
||||||
"credentialsSource": "local"
|
"credentialsSource": "local",
|
||||||
|
"config": "ios-production.yml"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
18
hooks/useControlsSafeAreaInsets.ts
Normal file
18
hooks/useControlsSafeAreaInsets.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import {
|
||||||
|
type EdgeInsets,
|
||||||
|
useSafeAreaInsets,
|
||||||
|
} from "react-native-safe-area-context";
|
||||||
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
|
|
||||||
|
const ZERO_INSETS: EdgeInsets = { top: 0, right: 0, bottom: 0, left: 0 };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns safe-area insets to apply to in-player controls, honoring the
|
||||||
|
* `safeAreaInControlsEnabled` user setting. When the setting is disabled,
|
||||||
|
* returns zero insets so controls can sit flush against the screen edges.
|
||||||
|
*/
|
||||||
|
export const useControlsSafeAreaInsets = (): EdgeInsets => {
|
||||||
|
const { settings } = useSettings();
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
return settings.safeAreaInControlsEnabled ? insets : ZERO_INSETS;
|
||||||
|
};
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { File, Paths } from "expo-file-system";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { storage } from "@/utils/mmkv";
|
import { storage } from "@/utils/mmkv";
|
||||||
|
|
||||||
@@ -12,36 +13,28 @@ const useImageStorage = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* expo-file-system instead of fetch+Blob+FileReader: the latter silently
|
||||||
|
* resolves to an empty payload under RN's New Architecture.
|
||||||
|
*/
|
||||||
const image2Base64 = useCallback(async (url?: string | null) => {
|
const image2Base64 = useCallback(async (url?: string | null) => {
|
||||||
if (!url) return null;
|
if (!url) return null;
|
||||||
|
|
||||||
let blob: Blob;
|
const tmpFile = new File(
|
||||||
|
Paths.cache,
|
||||||
|
`img-${Date.now()}-${Math.random().toString(36).slice(2)}.jpg`,
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
// Fetch the data from the URL
|
const downloaded = await File.downloadFileAsync(url, tmpFile, {
|
||||||
const response = await fetch(url);
|
idempotent: true,
|
||||||
blob = await response.blob();
|
});
|
||||||
|
return await downloaded.base64();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Error fetching image:", error);
|
console.warn("Error fetching image:", error);
|
||||||
return null;
|
return null;
|
||||||
|
} finally {
|
||||||
|
if (tmpFile.exists) tmpFile.delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a FileReader instance
|
|
||||||
const reader = new FileReader();
|
|
||||||
|
|
||||||
// Convert blob to base64
|
|
||||||
return new Promise<string>((resolve, reject) => {
|
|
||||||
reader.onloadend = () => {
|
|
||||||
if (typeof reader.result === "string") {
|
|
||||||
// Extract the base64 string (remove the data URL prefix)
|
|
||||||
const base64 = reader.result.split(",")[1];
|
|
||||||
resolve(base64);
|
|
||||||
} else {
|
|
||||||
reject(new Error("Failed to convert image to base64"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.onerror = reject;
|
|
||||||
reader.readAsDataURL(blob);
|
|
||||||
});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const saveImage = useCallback(
|
const saveImage = useCallback(
|
||||||
|
|||||||
22
hooks/useKeepWebSocketAlive.ts
Normal file
22
hooks/useKeepWebSocketAlive.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useWebSocketContext } from "@/providers/WebSocketProvider";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* While `active` is true, hold a keep-alive token on the global
|
||||||
|
* WebSocket so it is NOT closed when the app moves to
|
||||||
|
* background/inactive. Releases automatically when `active` flips
|
||||||
|
* false or the component unmounts.
|
||||||
|
*
|
||||||
|
* Used by the video player while in Picture-in-Picture so SyncPlay
|
||||||
|
* commands (and any other server pushes) keep flowing while the OS
|
||||||
|
* thinks the app is backgrounded.
|
||||||
|
*/
|
||||||
|
export function useKeepWebSocketAlive(active: boolean): void {
|
||||||
|
const { acquireKeepAlive } = useWebSocketContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!active) return;
|
||||||
|
const release = acquireKeepAlive();
|
||||||
|
return release;
|
||||||
|
}, [active, acquireKeepAlive]);
|
||||||
|
}
|
||||||
@@ -109,30 +109,35 @@ export const usePlaybackManager = ({
|
|||||||
staleTime: 0,
|
staleTime: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive prev/next from the current item's real position in the adjacent
|
||||||
|
* list rather than from the array length. `getEpisodes({ adjacentTo })` does
|
||||||
|
* not guarantee a fixed [prev, current, next] shape — at the first/last
|
||||||
|
* episode it can still return the current item as the first/last entry — so
|
||||||
|
* length-based indexing wrongly surfaces the current episode as "previous".
|
||||||
|
*/
|
||||||
|
const currentIndex = useMemo(
|
||||||
|
() => adjacentItems?.findIndex((e) => e.Id === item?.Id) ?? -1,
|
||||||
|
[adjacentItems, item],
|
||||||
|
);
|
||||||
|
|
||||||
|
/** A neighbour is only navigable if it has an actual media file (not a
|
||||||
|
* "Virtual"/missing episode placeholder, e.g. an absent Special). */
|
||||||
|
const isNavigable = (episode?: BaseItemDto | null): episode is BaseItemDto =>
|
||||||
|
!!episode && episode.Id !== item?.Id && episode.LocationType !== "Virtual";
|
||||||
|
|
||||||
const previousItem = useMemo(() => {
|
const previousItem = useMemo(() => {
|
||||||
if (!adjacentItems || adjacentItems.length <= 1) {
|
if (!adjacentItems || currentIndex <= 0) return null;
|
||||||
return null;
|
const candidate = adjacentItems[currentIndex - 1];
|
||||||
}
|
return isNavigable(candidate) ? candidate : null;
|
||||||
|
}, [adjacentItems, currentIndex, item]);
|
||||||
if (adjacentItems.length === 2) {
|
|
||||||
return adjacentItems[0].Id === item?.Id ? null : adjacentItems[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return adjacentItems[0];
|
|
||||||
}, [adjacentItems, item]);
|
|
||||||
|
|
||||||
/** The next item in the series */
|
/** The next item in the series */
|
||||||
const nextItem = useMemo(() => {
|
const nextItem = useMemo(() => {
|
||||||
if (!adjacentItems || adjacentItems.length <= 1) {
|
if (!adjacentItems || currentIndex < 0) return null;
|
||||||
return null;
|
const candidate = adjacentItems[currentIndex + 1];
|
||||||
}
|
return isNavigable(candidate) ? candidate : null;
|
||||||
|
}, [adjacentItems, currentIndex, item]);
|
||||||
if (adjacentItems.length === 2) {
|
|
||||||
return adjacentItems[1].Id === item?.Id ? null : adjacentItems[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
return adjacentItems[2];
|
|
||||||
}, [adjacentItems, item]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reports playback progress.
|
* Reports playback progress.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useEffect } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Alert } from "react-native";
|
import { Alert } from "react-native";
|
||||||
import useRouter from "@/hooks/useAppRouter";
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
|
import { useSyncPlay } from "@/providers/SyncPlay/SyncPlayProvider";
|
||||||
import { useWebSocketContext } from "@/providers/WebSocketProvider";
|
import { useWebSocketContext } from "@/providers/WebSocketProvider";
|
||||||
|
|
||||||
interface UseWebSocketProps {
|
interface UseWebSocketProps {
|
||||||
@@ -80,9 +81,9 @@ export const useWebSocket = ({
|
|||||||
playTrailers,
|
playTrailers,
|
||||||
}: UseWebSocketProps) => {
|
}: UseWebSocketProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { lastMessage } = useWebSocketContext();
|
const { lastMessage, clearLastMessage } = useWebSocketContext();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { clearLastMessage } = useWebSocketContext();
|
const { isEnabled: isSyncPlayEnabled } = useSyncPlay();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!lastMessage) return;
|
if (!lastMessage) return;
|
||||||
@@ -96,6 +97,25 @@ export const useWebSocket = ({
|
|||||||
| Record<string, string>
|
| Record<string, string>
|
||||||
| undefined; // Arguments are Dictionary<string, string>
|
| undefined; // Arguments are Dictionary<string, string>
|
||||||
|
|
||||||
|
// Skip playback commands when SyncPlay is enabled - SyncPlay handles these
|
||||||
|
const isSyncPlayCommand =
|
||||||
|
lastMessage.MessageType === "SyncPlayCommand" ||
|
||||||
|
lastMessage.MessageType === "SyncPlayGroupUpdate";
|
||||||
|
const isPlaybackCommand = [
|
||||||
|
"PlayPause",
|
||||||
|
"Pause",
|
||||||
|
"Unpause",
|
||||||
|
"Stop",
|
||||||
|
"Seek",
|
||||||
|
"NextTrack",
|
||||||
|
"PreviousTrack",
|
||||||
|
].includes(command ?? "");
|
||||||
|
|
||||||
|
if (isSyncPlayEnabled && (isSyncPlayCommand || isPlaybackCommand)) {
|
||||||
|
console.log(`Command ~ ${command} - skipping, SyncPlay handles playback`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (command === "PlayPause") {
|
if (command === "PlayPause") {
|
||||||
console.log("Command ~ PlayPause");
|
console.log("Command ~ PlayPause");
|
||||||
togglePlay();
|
togglePlay();
|
||||||
|
|||||||
@@ -1,46 +1,20 @@
|
|||||||
plugins {
|
apply plugin: 'expo-module-gradle-plugin'
|
||||||
id 'com.android.library'
|
|
||||||
id 'kotlin-android'
|
|
||||||
}
|
|
||||||
|
|
||||||
group = 'expo.modules.backgrounddownloader'
|
group = 'expo.modules.backgrounddownloader'
|
||||||
version = '1.0.0'
|
version = '1.0.0'
|
||||||
|
|
||||||
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
expoModule {
|
||||||
def kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.25'
|
canBePublished false
|
||||||
|
}
|
||||||
apply from: expoModulesCorePlugin
|
|
||||||
|
|
||||||
applyKotlinExpoModulesCorePlugin()
|
|
||||||
useDefaultAndroidSdkVersions()
|
|
||||||
useCoreDependencies()
|
|
||||||
useExpoPublishing()
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace "expo.modules.backgrounddownloader"
|
namespace "expo.modules.backgrounddownloader"
|
||||||
|
defaultConfig {
|
||||||
compileOptions {
|
versionCode 1
|
||||||
sourceCompatibility JavaVersion.VERSION_17
|
versionName "1.0.0"
|
||||||
targetCompatibility JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "17"
|
|
||||||
}
|
|
||||||
|
|
||||||
lintOptions {
|
|
||||||
abortOnError false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
|
||||||
implementation "com.squareup.okhttp3:okhttp:4.12.0"
|
implementation "com.squareup.okhttp3:okhttp:4.12.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "17"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,13 @@ class MpvPlayerModule : Module() {
|
|||||||
// No-op on Android - media session integration would require MediaSessionCompat
|
// No-op on Android - media session integration would require MediaSessionCompat
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When true, PiP play/pause/skip controls emit JS events
|
||||||
|
// instead of driving MPV directly, so the host app can route
|
||||||
|
// through SyncPlay (server -> group broadcast -> all clients).
|
||||||
|
Prop("syncPlayDelegated") { view: MpvPlayerView, delegated: Boolean ->
|
||||||
|
view.syncPlayDelegated = delegated
|
||||||
|
}
|
||||||
|
|
||||||
// Async function to play video
|
// Async function to play video
|
||||||
AsyncFunction("play") { view: MpvPlayerView ->
|
AsyncFunction("play") { view: MpvPlayerView ->
|
||||||
view.play()
|
view.play()
|
||||||
@@ -198,7 +205,7 @@ class MpvPlayerModule : Module() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Defines events that the view can send to JavaScript
|
// Defines events that the view can send to JavaScript
|
||||||
Events("onLoad", "onPlaybackStateChange", "onProgress", "onError", "onTracksReady", "onPictureInPictureChange")
|
Events("onLoad", "onPlaybackStateChange", "onProgress", "onError", "onTracksReady", "onPictureInPictureChange", "onPipPlayRequest", "onPipPauseRequest", "onPipSkipRequest")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,16 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
|||||||
val onError by EventDispatcher()
|
val onError by EventDispatcher()
|
||||||
val onTracksReady by EventDispatcher()
|
val onTracksReady by EventDispatcher()
|
||||||
val onPictureInPictureChange by EventDispatcher()
|
val onPictureInPictureChange by EventDispatcher()
|
||||||
|
// SyncPlay: when `syncPlayDelegated == true`, PiP playback controls
|
||||||
|
// (play / pause / skip) emit these events instead of driving MPV
|
||||||
|
// directly, so JS can route the action through the SyncPlay
|
||||||
|
// controller (server -> group broadcast -> all clients). Default
|
||||||
|
// behavior (non-SyncPlay) is unchanged.
|
||||||
|
val onPipPlayRequest by EventDispatcher()
|
||||||
|
val onPipPauseRequest by EventDispatcher()
|
||||||
|
val onPipSkipRequest by EventDispatcher()
|
||||||
|
|
||||||
|
var syncPlayDelegated: Boolean = false
|
||||||
|
|
||||||
private var textureView: TextureView
|
private var textureView: TextureView
|
||||||
private var renderer: MPVLayerRenderer? = null
|
private var renderer: MPVLayerRenderer? = null
|
||||||
@@ -85,14 +95,32 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
|||||||
pipController?.setPlayerView(textureView)
|
pipController?.setPlayerView(textureView)
|
||||||
pipController?.delegate = object : PiPController.Delegate {
|
pipController?.delegate = object : PiPController.Delegate {
|
||||||
override fun onPlay() {
|
override fun onPlay() {
|
||||||
|
if (syncPlayDelegated) {
|
||||||
|
onPipPlayRequest(mapOf<String, Any>())
|
||||||
|
return
|
||||||
|
}
|
||||||
play()
|
play()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
|
if (syncPlayDelegated) {
|
||||||
|
onPipPauseRequest(mapOf<String, Any>())
|
||||||
|
return
|
||||||
|
}
|
||||||
pause()
|
pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSeekBy(seconds: Double) {
|
override fun onSeekBy(seconds: Double) {
|
||||||
|
if (syncPlayDelegated) {
|
||||||
|
val target = (cachedPosition + seconds).coerceAtLeast(0.0)
|
||||||
|
onPipSkipRequest(
|
||||||
|
mapOf(
|
||||||
|
"targetSeconds" to target,
|
||||||
|
"intervalSeconds" to seconds
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
seekBy(seconds)
|
seekBy(seconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,13 @@ public class MpvPlayerModule: Module {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When true, PiP play/pause/skip controls emit JS events instead
|
||||||
|
// of driving MPV directly, so the host app can route through
|
||||||
|
// SyncPlay (server -> group broadcast -> all clients).
|
||||||
|
Prop("syncPlayDelegated") { (view: MpvPlayerView, delegated: Bool) in
|
||||||
|
view.syncPlayDelegated = delegated
|
||||||
|
}
|
||||||
|
|
||||||
// Async function to play video
|
// Async function to play video
|
||||||
AsyncFunction("play") { (view: MpvPlayerView) in
|
AsyncFunction("play") { (view: MpvPlayerView) in
|
||||||
view.play()
|
view.play()
|
||||||
@@ -213,7 +220,7 @@ public class MpvPlayerModule: Module {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Defines events that the view can send to JavaScript
|
// Defines events that the view can send to JavaScript
|
||||||
Events("onLoad", "onPlaybackStateChange", "onProgress", "onError", "onTracksReady")
|
Events("onLoad", "onPlaybackStateChange", "onProgress", "onError", "onTracksReady", "onPictureInPictureChange", "onPipPlayRequest", "onPipPauseRequest", "onPipSkipRequest")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,17 @@ class MpvPlayerView: ExpoView {
|
|||||||
let onProgress = EventDispatcher()
|
let onProgress = EventDispatcher()
|
||||||
let onError = EventDispatcher()
|
let onError = EventDispatcher()
|
||||||
let onTracksReady = EventDispatcher()
|
let onTracksReady = EventDispatcher()
|
||||||
|
let onPictureInPictureChange = EventDispatcher()
|
||||||
|
// SyncPlay: when `syncPlayDelegated == true`, PiP playback controls
|
||||||
|
// (play / pause / skip) emit these events instead of driving MPV
|
||||||
|
// directly, so JS can route the action through the SyncPlay
|
||||||
|
// controller (server → group broadcast → all clients). Default
|
||||||
|
// behavior (non-SyncPlay) is unchanged.
|
||||||
|
let onPipPlayRequest = EventDispatcher()
|
||||||
|
let onPipPauseRequest = EventDispatcher()
|
||||||
|
let onPipSkipRequest = EventDispatcher()
|
||||||
|
|
||||||
|
var syncPlayDelegated: Bool = false
|
||||||
|
|
||||||
private var currentURL: URL?
|
private var currentURL: URL?
|
||||||
private var cachedPosition: Double = 0
|
private var cachedPosition: Double = 0
|
||||||
@@ -81,7 +92,6 @@ class MpvPlayerView: ExpoView {
|
|||||||
private func setupView() {
|
private func setupView() {
|
||||||
clipsToBounds = true
|
clipsToBounds = true
|
||||||
backgroundColor = .black
|
backgroundColor = .black
|
||||||
configureAudioSession()
|
|
||||||
|
|
||||||
videoContainer = UIView()
|
videoContainer = UIView()
|
||||||
videoContainer.translatesAutoresizingMaskIntoConstraints = false
|
videoContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||||
@@ -141,21 +151,26 @@ class MpvPlayerView: ExpoView {
|
|||||||
CATransaction.commit()
|
CATransaction.commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Audio Session & Notifications
|
||||||
|
|
||||||
private func configureAudioSession() {
|
private func configureAudioSession() {
|
||||||
let audioSession = AVAudioSession.sharedInstance()
|
let session = AVAudioSession.sharedInstance()
|
||||||
do {
|
do {
|
||||||
try audioSession.setCategory(
|
try session.setCategory(.playback, mode: .moviePlayback, policy: .longFormAudio, options: [])
|
||||||
.playback,
|
try session.setActive(true)
|
||||||
mode: .moviePlayback,
|
|
||||||
policy: .longFormAudio,
|
|
||||||
options: []
|
|
||||||
)
|
|
||||||
try audioSession.setActive(true)
|
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to configure audio session: \(error)")
|
print("Failed to configure audio session: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// MARK: - Audio Session & Notifications
|
|
||||||
|
/// Deactivate the session AND reset the category — `setActive(false)` alone
|
||||||
|
/// leaves `.playback`/`.longFormAudio` on the shared singleton, so any later
|
||||||
|
/// reactivation (foreground, route change, other modules) re-steals audio.
|
||||||
|
private func tearDownAudioSession() {
|
||||||
|
let session = AVAudioSession.sharedInstance()
|
||||||
|
try? session.setActive(false, options: .notifyOthersOnDeactivation)
|
||||||
|
try? session.setCategory(.ambient, mode: .default, options: [.mixWithOthers])
|
||||||
|
}
|
||||||
|
|
||||||
private func setupNotifications() {
|
private func setupNotifications() {
|
||||||
// Handle audio session interruptions (e.g., incoming calls, other apps playing audio)
|
// Handle audio session interruptions (e.g., incoming calls, other apps playing audio)
|
||||||
@@ -270,6 +285,7 @@ class MpvPlayerView: ExpoView {
|
|||||||
|
|
||||||
func play() {
|
func play() {
|
||||||
intendedPlayState = true
|
intendedPlayState = true
|
||||||
|
configureAudioSession()
|
||||||
setupRemoteCommands()
|
setupRemoteCommands()
|
||||||
renderer?.play()
|
renderer?.play()
|
||||||
pipController?.setPlaybackRate(1.0)
|
pipController?.setPlaybackRate(1.0)
|
||||||
@@ -440,6 +456,7 @@ class MpvPlayerView: ExpoView {
|
|||||||
renderer?.stop()
|
renderer?.stop()
|
||||||
displayLayer.removeFromSuperlayer()
|
displayLayer.removeFromSuperlayer()
|
||||||
clearNowPlayingInfo()
|
clearNowPlayingInfo()
|
||||||
|
tearDownAudioSession()
|
||||||
NotificationCenter.default.removeObserver(self)
|
NotificationCenter.default.removeObserver(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -519,9 +536,7 @@ extension MpvPlayerView: MPVLayerRendererDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func renderer(_: MPVLayerRenderer, didSelectAudioOutput audioOutput: String) {
|
func renderer(_: MPVLayerRenderer, didSelectAudioOutput audioOutput: String) {
|
||||||
// Audio output is now active - this is the right time to activate audio session and set Now Playing
|
print("[MPV] Audio output ready (\(audioOutput)), syncing Now Playing")
|
||||||
print("[MPV] Audio output ready (\(audioOutput)), activating audio session and syncing Now Playing")
|
|
||||||
nowPlayingManager.activateAudioSession()
|
|
||||||
syncNowPlaying(isPlaying: !isPaused())
|
syncNowPlaying(isPlaying: !isPaused())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -633,6 +648,9 @@ extension MpvPlayerView: PiPControllerDelegate {
|
|||||||
print("PiP did start: \(didStartPictureInPicture)")
|
print("PiP did start: \(didStartPictureInPicture)")
|
||||||
// Ensure current time is synced when PiP starts
|
// Ensure current time is synced when PiP starts
|
||||||
pipController?.setCurrentTimeFromSeconds(cachedPosition, duration: cachedDuration)
|
pipController?.setCurrentTimeFromSeconds(cachedPosition, duration: cachedDuration)
|
||||||
|
// Notify JS of the actual PiP active state. `didStartPictureInPicture`
|
||||||
|
// is `false` when AVKit reports a failure to start, so reflect that.
|
||||||
|
onPictureInPictureChange(["isActive": didStartPictureInPicture])
|
||||||
}
|
}
|
||||||
|
|
||||||
func pipController(_ controller: PiPController, willStopPictureInPicture: Bool) {
|
func pipController(_ controller: PiPController, willStopPictureInPicture: Bool) {
|
||||||
@@ -651,6 +669,9 @@ extension MpvPlayerView: PiPControllerDelegate {
|
|||||||
if _isZoomedToFill {
|
if _isZoomedToFill {
|
||||||
displayLayer.videoGravity = .resizeAspectFill
|
displayLayer.videoGravity = .resizeAspectFill
|
||||||
}
|
}
|
||||||
|
// Notify JS that PiP has fully stopped so the controls overlay can
|
||||||
|
// be re-mounted when the user returns to full screen.
|
||||||
|
onPictureInPictureChange(["isActive": false])
|
||||||
}
|
}
|
||||||
|
|
||||||
func pipController(_ controller: PiPController, restoreUserInterfaceForPictureInPictureStop completionHandler: @escaping (Bool) -> Void) {
|
func pipController(_ controller: PiPController, restoreUserInterfaceForPictureInPictureStop completionHandler: @escaping (Bool) -> Void) {
|
||||||
@@ -660,6 +681,12 @@ extension MpvPlayerView: PiPControllerDelegate {
|
|||||||
|
|
||||||
func pipControllerPlay(_ controller: PiPController) {
|
func pipControllerPlay(_ controller: PiPController) {
|
||||||
print("PiP play requested")
|
print("PiP play requested")
|
||||||
|
if syncPlayDelegated {
|
||||||
|
// Let JS route through SyncPlay. We deliberately do NOT touch
|
||||||
|
// MPV here; the WS command coming back will drive playback.
|
||||||
|
onPipPlayRequest([:])
|
||||||
|
return
|
||||||
|
}
|
||||||
intendedPlayState = true
|
intendedPlayState = true
|
||||||
renderer?.play()
|
renderer?.play()
|
||||||
pipController?.setPlaybackRate(1.0)
|
pipController?.setPlaybackRate(1.0)
|
||||||
@@ -667,6 +694,10 @@ extension MpvPlayerView: PiPControllerDelegate {
|
|||||||
|
|
||||||
func pipControllerPause(_ controller: PiPController) {
|
func pipControllerPause(_ controller: PiPController) {
|
||||||
print("PiP pause requested")
|
print("PiP pause requested")
|
||||||
|
if syncPlayDelegated {
|
||||||
|
onPipPauseRequest([:])
|
||||||
|
return
|
||||||
|
}
|
||||||
intendedPlayState = false
|
intendedPlayState = false
|
||||||
renderer?.pausePlayback()
|
renderer?.pausePlayback()
|
||||||
pipController?.setPlaybackRate(0.0)
|
pipController?.setPlaybackRate(0.0)
|
||||||
@@ -676,6 +707,16 @@ extension MpvPlayerView: PiPControllerDelegate {
|
|||||||
let seconds = CMTimeGetSeconds(interval)
|
let seconds = CMTimeGetSeconds(interval)
|
||||||
print("PiP skip by interval: \(seconds)")
|
print("PiP skip by interval: \(seconds)")
|
||||||
let target = max(0, cachedPosition + seconds)
|
let target = max(0, cachedPosition + seconds)
|
||||||
|
if syncPlayDelegated {
|
||||||
|
// `targetSeconds` lets JS convert to ticks and call
|
||||||
|
// syncPlayController.seek(). `intervalSeconds` is also sent
|
||||||
|
// for telemetry / debug.
|
||||||
|
onPipSkipRequest([
|
||||||
|
"targetSeconds": target,
|
||||||
|
"intervalSeconds": seconds
|
||||||
|
])
|
||||||
|
return
|
||||||
|
}
|
||||||
seekTo(position: target)
|
seekTo(position: target)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,21 @@ export type OnPictureInPictureChangePayload = {
|
|||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted when the user taps a PiP playback control while the view
|
||||||
|
* was rendered with `syncPlayDelegated`. The host app should route
|
||||||
|
* the action through the SyncPlay controller instead of acting
|
||||||
|
* locally.
|
||||||
|
*/
|
||||||
|
export type OnPipPlayRequestPayload = Record<string, never>;
|
||||||
|
export type OnPipPauseRequestPayload = Record<string, never>;
|
||||||
|
export type OnPipSkipRequestPayload = {
|
||||||
|
/** Absolute target position the user wants to seek to, in seconds. */
|
||||||
|
targetSeconds: number;
|
||||||
|
/** Skip interval requested by the OS (signed seconds). Debug only. */
|
||||||
|
intervalSeconds: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type NowPlayingMetadata = {
|
export type NowPlayingMetadata = {
|
||||||
title?: string;
|
title?: string;
|
||||||
artist?: string;
|
artist?: string;
|
||||||
@@ -84,6 +99,18 @@ export type MpvPlayerViewProps = {
|
|||||||
onPictureInPictureChange?: (event: {
|
onPictureInPictureChange?: (event: {
|
||||||
nativeEvent: OnPictureInPictureChangePayload;
|
nativeEvent: OnPictureInPictureChangePayload;
|
||||||
}) => void;
|
}) => void;
|
||||||
|
/**
|
||||||
|
* When true, PiP play/pause/skip controls emit the corresponding
|
||||||
|
* `onPipPlayRequest` / `onPipPauseRequest` / `onPipSkipRequest`
|
||||||
|
* events instead of driving MPV directly. Used to route PiP control
|
||||||
|
* actions through SyncPlay.
|
||||||
|
*/
|
||||||
|
syncPlayDelegated?: boolean;
|
||||||
|
onPipPlayRequest?: (event: { nativeEvent: OnPipPlayRequestPayload }) => void;
|
||||||
|
onPipPauseRequest?: (event: {
|
||||||
|
nativeEvent: OnPipPauseRequestPayload;
|
||||||
|
}) => void;
|
||||||
|
onPipSkipRequest?: (event: { nativeEvent: OnPipSkipRequestPayload }) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface MpvPlayerViewRef {
|
export interface MpvPlayerViewRef {
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { MpvPlayerViewProps } from "./MpvPlayer.types";
|
import { MpvPlayerViewProps } from "./MpvPlayer.types";
|
||||||
|
|
||||||
export default function MpvPlayerView(props: MpvPlayerViewProps) {
|
export default function MpvPlayerView(props: MpvPlayerViewProps) {
|
||||||
const url = props.source?.url ?? "";
|
const url = props.source?.url ?? "";
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<iframe
|
<iframe
|
||||||
title='MPV Player'
|
title={t("player.mpv_player_title")}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
src={url}
|
src={url}
|
||||||
onLoad={() => props.onLoad?.({ nativeEvent: { url } })}
|
onLoad={() => props.onLoad?.({ nativeEvent: { url } })}
|
||||||
|
|||||||
@@ -4,9 +4,16 @@ const { withEntitlementsPlist } = require("expo/config-plugins");
|
|||||||
* Expo config plugin to add User Management entitlement for tvOS profile linking
|
* Expo config plugin to add User Management entitlement for tvOS profile linking
|
||||||
*/
|
*/
|
||||||
const withTVUserManagement = (config) => {
|
const withTVUserManagement = (config) => {
|
||||||
|
// Only add for tvOS builds. The entitlement is restricted by Apple and must
|
||||||
|
// be present in the provisioning profile, so injecting it into mobile builds
|
||||||
|
// breaks signing ("Entitlement ... not found and could not be included in
|
||||||
|
// profile"). The entitlement is only needed for tvOS
|
||||||
|
// TVUserManager.currentUserIdentifier.
|
||||||
|
if (process.env.EXPO_TV !== "1") {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
return withEntitlementsPlist(config, (config) => {
|
return withEntitlementsPlist(config, (config) => {
|
||||||
// Only add for tvOS builds (check if building for TV)
|
|
||||||
// The entitlement is needed for TVUserManager.currentUserIdentifier to work
|
|
||||||
config.modResults["com.apple.developer.user-management"] = [
|
config.modResults["com.apple.developer.user-management"] = [
|
||||||
"runs-as-current-user",
|
"runs-as-current-user",
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -4,28 +4,68 @@ import type { DownloadedItem, DownloadsDatabase } from "./types";
|
|||||||
|
|
||||||
const DOWNLOADS_DATABASE_KEY = "downloads.v2.json";
|
const DOWNLOADS_DATABASE_KEY = "downloads.v2.json";
|
||||||
|
|
||||||
|
// Performance optimization: Cache the parsed database to avoid repeated JSON.parse calls
|
||||||
|
let cachedDb: DownloadsDatabase | null = null;
|
||||||
|
let cacheVersion = 0;
|
||||||
|
|
||||||
|
// Performance optimization: Cache the flattened items array
|
||||||
|
let cachedItems: DownloadedItem[] | null = null;
|
||||||
|
let itemsCacheVersion = -1;
|
||||||
|
|
||||||
|
// Performance optimization: Index for O(1) item lookups by ID
|
||||||
|
let itemIndex: Map<string, DownloadedItem> | null = null;
|
||||||
|
let indexCacheVersion = -1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the downloads database from storage
|
* Get the downloads database from storage
|
||||||
|
* PERFORMANCE: Caches the parsed database to avoid repeated JSON.parse calls.
|
||||||
|
* NOTE: Returns the shared cached instance — do NOT mutate it directly. Go
|
||||||
|
* through addDownloadedItem/updateDownloadedItem/removeDownloadedItem so
|
||||||
|
* saveDownloadsDatabase() runs and the derived caches stay consistent.
|
||||||
*/
|
*/
|
||||||
export function getDownloadsDatabase(): DownloadsDatabase {
|
export function getDownloadsDatabase(): DownloadsDatabase {
|
||||||
|
// Return cached database if available
|
||||||
|
if (cachedDb !== null) {
|
||||||
|
return cachedDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse from storage and cache the result
|
||||||
const file = storage.getString(DOWNLOADS_DATABASE_KEY);
|
const file = storage.getString(DOWNLOADS_DATABASE_KEY);
|
||||||
if (file) {
|
if (file) {
|
||||||
return JSON.parse(file) as DownloadsDatabase;
|
cachedDb = JSON.parse(file) as DownloadsDatabase;
|
||||||
|
return cachedDb;
|
||||||
}
|
}
|
||||||
return { movies: {}, series: {}, other: {} };
|
|
||||||
|
const emptyDb = { movies: {}, series: {}, other: {} };
|
||||||
|
cachedDb = emptyDb;
|
||||||
|
return emptyDb;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save the downloads database to storage
|
* Save the downloads database to storage
|
||||||
|
* PERFORMANCE: Updates cache and invalidates derived caches
|
||||||
*/
|
*/
|
||||||
export function saveDownloadsDatabase(db: DownloadsDatabase): void {
|
export function saveDownloadsDatabase(db: DownloadsDatabase): void {
|
||||||
storage.set(DOWNLOADS_DATABASE_KEY, JSON.stringify(db));
|
storage.set(DOWNLOADS_DATABASE_KEY, JSON.stringify(db));
|
||||||
|
// Update the cache with the new database
|
||||||
|
cachedDb = db;
|
||||||
|
// Invalidate derived caches (items array and index)
|
||||||
|
cachedItems = null;
|
||||||
|
itemIndex = null;
|
||||||
|
cacheVersion++;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all downloaded items as a flat array
|
* Get all downloaded items as a flat array
|
||||||
|
* PERFORMANCE: Caches the flattened array to avoid rebuilding on every call
|
||||||
*/
|
*/
|
||||||
export function getAllDownloadedItems(): DownloadedItem[] {
|
export function getAllDownloadedItems(): DownloadedItem[] {
|
||||||
|
// Return cached items if available and up-to-date
|
||||||
|
if (cachedItems !== null && itemsCacheVersion === cacheVersion) {
|
||||||
|
return cachedItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the items array from the database
|
||||||
const db = getDownloadsDatabase();
|
const db = getDownloadsDatabase();
|
||||||
const items: DownloadedItem[] = [];
|
const items: DownloadedItem[] = [];
|
||||||
|
|
||||||
@@ -47,34 +87,41 @@ export function getAllDownloadedItems(): DownloadedItem[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
cachedItems = items;
|
||||||
|
itemsCacheVersion = cacheVersion;
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a downloaded item by its ID
|
* Build or refresh the item index for O(1) lookups
|
||||||
*/
|
*/
|
||||||
export function getDownloadedItemById(id: string): DownloadedItem | undefined {
|
function ensureItemIndex(): void {
|
||||||
const db = getDownloadsDatabase();
|
if (itemIndex !== null && indexCacheVersion === cacheVersion) {
|
||||||
|
return; // Index is up-to-date
|
||||||
if (db.movies[id]) {
|
|
||||||
return db.movies[id];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const series of Object.values(db.series)) {
|
// Build new index from all items
|
||||||
for (const season of Object.values(series.seasons)) {
|
itemIndex = new Map<string, DownloadedItem>();
|
||||||
for (const episode of Object.values(season.episodes)) {
|
const items = getAllDownloadedItems();
|
||||||
if (episode.item.Id === id) {
|
|
||||||
return episode;
|
for (const item of items) {
|
||||||
}
|
if (item.item.Id) {
|
||||||
}
|
itemIndex.set(item.item.Id, item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (db.other?.[id]) {
|
indexCacheVersion = cacheVersion;
|
||||||
return db.other[id];
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
/**
|
||||||
|
* Get a downloaded item by its ID
|
||||||
|
* PERFORMANCE: Uses O(1) index lookup instead of O(n²) iteration
|
||||||
|
*/
|
||||||
|
export function getDownloadedItemById(id: string): DownloadedItem | undefined {
|
||||||
|
ensureItemIndex();
|
||||||
|
return itemIndex!.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -221,4 +268,5 @@ export function updateDownloadedItem(
|
|||||||
*/
|
*/
|
||||||
export function clearAllDownloadedItems(): void {
|
export function clearAllDownloadedItems(): void {
|
||||||
saveDownloadsDatabase({ movies: {}, series: {}, other: {} });
|
saveDownloadsDatabase({ movies: {}, series: {}, other: {} });
|
||||||
|
// saveDownloadsDatabase already invalidates caches
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -619,44 +619,54 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
setUser(storedUser);
|
setUser(storedUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await getUserApi(apiInstance).getCurrentUser();
|
// Dismiss splash screen with cached data immediately,
|
||||||
setUser(response.data);
|
// fetch fresh user data in the background
|
||||||
|
setInitialLoaded(true);
|
||||||
|
|
||||||
// Migrate current session to secure storage if not already saved
|
try {
|
||||||
if (storedUser?.Id && storedUser?.Name) {
|
const response = await getUserApi(apiInstance).getCurrentUser();
|
||||||
const existingCredential = await getAccountCredential(
|
setUser(response.data);
|
||||||
serverUrl,
|
|
||||||
storedUser.Id,
|
// Migrate current session to secure storage if not already saved
|
||||||
);
|
if (storedUser?.Id && storedUser?.Name) {
|
||||||
if (!existingCredential) {
|
const existingCredential = await getAccountCredential(
|
||||||
await saveAccountCredential({
|
|
||||||
serverUrl,
|
serverUrl,
|
||||||
serverName: "",
|
storedUser.Id,
|
||||||
token,
|
);
|
||||||
userId: storedUser.Id,
|
if (!existingCredential) {
|
||||||
username: storedUser.Name,
|
await saveAccountCredential({
|
||||||
savedAt: Date.now(),
|
serverUrl,
|
||||||
securityType: "none",
|
serverName: "",
|
||||||
primaryImageTag: response.data.PrimaryImageTag ?? undefined,
|
token,
|
||||||
});
|
userId: storedUser.Id,
|
||||||
} else if (
|
username: storedUser.Name,
|
||||||
response.data.PrimaryImageTag !==
|
savedAt: Date.now(),
|
||||||
existingCredential.primaryImageTag
|
securityType: "none",
|
||||||
) {
|
primaryImageTag: response.data.PrimaryImageTag ?? undefined,
|
||||||
// Update image tag if it has changed
|
});
|
||||||
addAccountToServer(serverUrl, existingCredential.serverName, {
|
} else if (
|
||||||
userId: existingCredential.userId,
|
response.data.PrimaryImageTag !==
|
||||||
username: existingCredential.username,
|
existingCredential.primaryImageTag
|
||||||
securityType: existingCredential.securityType,
|
) {
|
||||||
savedAt: existingCredential.savedAt,
|
// Update image tag if it has changed
|
||||||
primaryImageTag: response.data.PrimaryImageTag ?? undefined,
|
addAccountToServer(serverUrl, existingCredential.serverName, {
|
||||||
});
|
userId: existingCredential.userId,
|
||||||
|
username: existingCredential.username,
|
||||||
|
securityType: existingCredential.securityType,
|
||||||
|
savedAt: existingCredential.savedAt,
|
||||||
|
primaryImageTag: response.data.PrimaryImageTag ?? undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Background fetch failed — app already rendered with cached data
|
||||||
|
console.warn("Background user fetch failed, using cached data:", e);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
setInitialLoaded(true);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
} finally {
|
|
||||||
setInitialLoaded(true);
|
setInitialLoaded(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
165
providers/SyncPlay/Controller.ts
Normal file
165
providers/SyncPlay/Controller.ts
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
/**
|
||||||
|
* SyncPlay Controller — public playback API exposed to consumers.
|
||||||
|
*
|
||||||
|
* Methods are fire-and-forget by design: SyncPlay HTTP responses don't
|
||||||
|
* carry useful info (the real state arrives via WebSocket broadcast).
|
||||||
|
* Wrap calls in try/catch so transient network errors don't reach the UI.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
|
import { getSyncPlayApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
|
import type { SyncPlayManager } from "./Manager";
|
||||||
|
import {
|
||||||
|
getItemsForPlayback,
|
||||||
|
type TranslateOptions,
|
||||||
|
translateItemsForPlayback,
|
||||||
|
} from "./transport/queueTranslation";
|
||||||
|
|
||||||
|
export interface PlayOptions extends TranslateOptions {
|
||||||
|
items?: BaseItemDto[];
|
||||||
|
ids?: string[];
|
||||||
|
startIndex?: number;
|
||||||
|
startPositionTicks?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Controller {
|
||||||
|
private manager!: SyncPlayManager;
|
||||||
|
|
||||||
|
init(manager: SyncPlayManager): void {
|
||||||
|
this.manager = manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Toggle play/pause for the whole group. */
|
||||||
|
playPause(): void {
|
||||||
|
if (this.manager.isPlaying()) {
|
||||||
|
this.pause();
|
||||||
|
} else {
|
||||||
|
this.unpause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resume the group's playback. */
|
||||||
|
unpause(): void {
|
||||||
|
this.manager.markPendingPlaybackCommand("Unpause");
|
||||||
|
try {
|
||||||
|
getSyncPlayApi(this.manager.getApiClient()).syncPlayUnpause();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("SyncPlay Controller.unpause failed", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pause the group's playback. */
|
||||||
|
pause(): void {
|
||||||
|
this.manager.markPendingPlaybackCommand("Pause");
|
||||||
|
try {
|
||||||
|
getSyncPlayApi(this.manager.getApiClient()).syncPlayPause();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("SyncPlay Controller.pause failed", error);
|
||||||
|
}
|
||||||
|
// Pause locally too so the user sees instant feedback.
|
||||||
|
this.manager.getPlayerWrapper().localPause();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Seek the group's playback. `positionTicks` is in ticks (1ms = 10000 ticks). */
|
||||||
|
seek(positionTicks: number): void {
|
||||||
|
try {
|
||||||
|
getSyncPlayApi(this.manager.getApiClient()).syncPlaySeek({
|
||||||
|
seekRequestDto: { PositionTicks: positionTicks },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("SyncPlay Controller.seek failed", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start playback in the group. Expands containers (Series, Season,
|
||||||
|
* BoxSet, Playlist, single Episode w/ autoplay) into the real
|
||||||
|
* playable queue before broadcasting.
|
||||||
|
*
|
||||||
|
* Resolves once the SetNewQueue request completes; the server then
|
||||||
|
* broadcasts a PlayQueue update and Play command to every member.
|
||||||
|
*/
|
||||||
|
async play(options: PlayOptions): Promise<void> {
|
||||||
|
const api = this.manager.getApiClient();
|
||||||
|
|
||||||
|
const sendPlayRequest = async (items: BaseItemDto[]) => {
|
||||||
|
const queue = items
|
||||||
|
.map((item) => item.Id)
|
||||||
|
.filter((id): id is string => typeof id === "string");
|
||||||
|
await getSyncPlayApi(api).syncPlaySetNewQueue({
|
||||||
|
playRequestDto: {
|
||||||
|
PlayingQueue: queue,
|
||||||
|
PlayingItemPosition: options.startIndex ?? 0,
|
||||||
|
StartPositionTicks: options.startPositionTicks ?? 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sourceItems = options.items
|
||||||
|
? options.items
|
||||||
|
: await getItemsForPlayback(api, options.ids ?? []);
|
||||||
|
const items = await translateItemsForPlayback(api, sourceItems, options);
|
||||||
|
await sendPlayRequest(items);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("SyncPlay Controller.play failed", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stop the group's playback. */
|
||||||
|
stop(): void {
|
||||||
|
try {
|
||||||
|
getSyncPlayApi(this.manager.getApiClient()).syncPlayStop();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("SyncPlay Controller.stop failed", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Jump to the next item in the group's queue. */
|
||||||
|
nextItem(): void {
|
||||||
|
try {
|
||||||
|
getSyncPlayApi(this.manager.getApiClient()).syncPlayNextItem({
|
||||||
|
nextItemRequestDto: {
|
||||||
|
PlaylistItemId: this.manager
|
||||||
|
.getQueueCore()
|
||||||
|
.getCurrentPlaylistItemId(),
|
||||||
|
} as unknown as Parameters<
|
||||||
|
ReturnType<typeof getSyncPlayApi>["syncPlayNextItem"]
|
||||||
|
>[0]["nextItemRequestDto"],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("SyncPlay Controller.nextItem failed", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Jump to the previous item in the group's queue. */
|
||||||
|
previousItem(): void {
|
||||||
|
try {
|
||||||
|
getSyncPlayApi(this.manager.getApiClient()).syncPlayPreviousItem({
|
||||||
|
previousItemRequestDto: {
|
||||||
|
PlaylistItemId: this.manager
|
||||||
|
.getQueueCore()
|
||||||
|
.getCurrentPlaylistItemId(),
|
||||||
|
} as unknown as Parameters<
|
||||||
|
ReturnType<typeof getSyncPlayApi>["syncPlayPreviousItem"]
|
||||||
|
>[0]["previousItemRequestDto"],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("SyncPlay Controller.previousItem failed", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Jump to a specific item in the queue by playlist item id. */
|
||||||
|
setCurrentPlaylistItem(playlistItemId: string): void {
|
||||||
|
try {
|
||||||
|
getSyncPlayApi(this.manager.getApiClient()).syncPlaySetPlaylistItem({
|
||||||
|
setPlaylistItemRequestDto: { PlaylistItemId: playlistItemId },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("SyncPlay Controller.setCurrentPlaylistItem failed", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Controller;
|
||||||
93
providers/SyncPlay/EventEmitter.ts
Normal file
93
providers/SyncPlay/EventEmitter.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* Per-instance event emitter — replaces jellyfin-web's global `Events.trigger`
|
||||||
|
* bus. Listeners that throw are caught and logged so one bad listener can't
|
||||||
|
* break the rest.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { WaitForEventDefaultTimeout } from "./constants";
|
||||||
|
|
||||||
|
export class EventEmitter {
|
||||||
|
private listeners: Map<string, Set<(...args: unknown[]) => void>> = new Map();
|
||||||
|
|
||||||
|
on(event: string, callback: (...args: unknown[]) => void): void {
|
||||||
|
if (!this.listeners.has(event)) {
|
||||||
|
this.listeners.set(event, new Set());
|
||||||
|
}
|
||||||
|
this.listeners.get(event)!.add(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
off(event: string, callback: (...args: unknown[]) => void): void {
|
||||||
|
this.listeners.get(event)?.delete(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(event: string, ...args: unknown[]): void {
|
||||||
|
this.listeners.get(event)?.forEach((callback) => {
|
||||||
|
try {
|
||||||
|
callback(...args);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`SyncPlay EventEmitter: handler for "${event}" threw`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAllListeners(event?: string): void {
|
||||||
|
if (event) {
|
||||||
|
this.listeners.delete(event);
|
||||||
|
} else {
|
||||||
|
this.listeners.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve on the next emission of `event`, or reject after `timeoutMs`
|
||||||
|
* (or any event in `rejectEventTypes`). Cleans up every listener.
|
||||||
|
*/
|
||||||
|
export function waitForEventOnce(
|
||||||
|
emitter: EventEmitter,
|
||||||
|
event: string,
|
||||||
|
timeoutMs: number = WaitForEventDefaultTimeout,
|
||||||
|
rejectEventTypes?: string[],
|
||||||
|
): Promise<unknown[]> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
const clearAll = () => {
|
||||||
|
emitter.off(event, handler);
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
if (Array.isArray(rejectEventTypes)) {
|
||||||
|
for (const eventName of rejectEventTypes) {
|
||||||
|
emitter.off(eventName, rejectCallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handler = (...args: unknown[]) => {
|
||||||
|
clearAll();
|
||||||
|
resolve(args);
|
||||||
|
};
|
||||||
|
|
||||||
|
const rejectCallback = (...args: unknown[]) => {
|
||||||
|
clearAll();
|
||||||
|
reject(args[0] ?? new Error("rejected"));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (timeoutMs) {
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
clearAll();
|
||||||
|
reject(new Error("Timed out."));
|
||||||
|
}, timeoutMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
emitter.on(event, handler);
|
||||||
|
|
||||||
|
if (Array.isArray(rejectEventTypes)) {
|
||||||
|
for (const eventName of rejectEventTypes) {
|
||||||
|
emitter.on(eventName, rejectCallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
383
providers/SyncPlay/Manager.ts
Normal file
383
providers/SyncPlay/Manager.ts
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
/**
|
||||||
|
* SyncPlayManager — central orchestrator for a SyncPlay session.
|
||||||
|
*
|
||||||
|
* Owns the three "cores" (TimeSync, PlaybackCore, QueueCore) and the
|
||||||
|
* PlayerWrapper, and routes WebSocket events between them.
|
||||||
|
*
|
||||||
|
* Lifecycle:
|
||||||
|
* constructor → init() → (joinGroup → group-state-change "Idle"+) →
|
||||||
|
* group-state-change "Playing" → group-state-change "Paused" → ...
|
||||||
|
* → (leaveGroup) → destroy()
|
||||||
|
*
|
||||||
|
* Events emitted (provider listens):
|
||||||
|
* - `group-info-update` `(GroupInfoDto | null)`
|
||||||
|
* - `group-state-change` `(state: string, oldState: string)`
|
||||||
|
* - `enabled` `(isEnabled: boolean)`
|
||||||
|
* - `play-state-change` `(isFollowing: boolean)`
|
||||||
|
* - `playbackstart` / `playbackerror` — from PlayerWrapper hooks
|
||||||
|
* - `osd` `(action: SyncPlayOsdAction)`
|
||||||
|
* - `toast` `(messageKey: string)`
|
||||||
|
*
|
||||||
|
* The manager exposes a per-instance `EventEmitter` rather than upstream
|
||||||
|
* `Events.on(manager, ...)` — replaces the global Events bus pattern.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Api } from "@jellyfin/sdk";
|
||||||
|
import { Controller } from "./Controller";
|
||||||
|
import { PlaybackCore } from "./cores/PlaybackCore";
|
||||||
|
import { QueueCore } from "./cores/QueueCore";
|
||||||
|
import { TimeSync } from "./cores/TimeSync";
|
||||||
|
import { EventEmitter } from "./EventEmitter";
|
||||||
|
import { PendingPlaybackTracker } from "./player/PendingPlaybackTracker";
|
||||||
|
import { PlayerWrapper } from "./player/PlayerWrapper";
|
||||||
|
import { reconcileToGroupOnAttach } from "./player/reconcileToGroupOnAttach";
|
||||||
|
import type {
|
||||||
|
GroupInfoDto,
|
||||||
|
GroupUpdate,
|
||||||
|
PlayerControls,
|
||||||
|
PlayQueueUpdate,
|
||||||
|
SendCommand,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
/** Raw WebSocket message data shapes (already unwrapped by the hook). */
|
||||||
|
|
||||||
|
export class SyncPlayManager extends EventEmitter {
|
||||||
|
private apiClient: Api;
|
||||||
|
private playerWrapper: PlayerWrapper;
|
||||||
|
private timeSync: TimeSync;
|
||||||
|
private playbackCore: PlaybackCore;
|
||||||
|
private queueCore: QueueCore;
|
||||||
|
private pendingPlaybackTracker: PendingPlaybackTracker;
|
||||||
|
private controller: Controller;
|
||||||
|
|
||||||
|
/** Current group info. `null` when not in a group. */
|
||||||
|
private groupInfo: GroupInfoDto | null = null;
|
||||||
|
/** Is SyncPlay actively enabled (i.e., we're in a group)? */
|
||||||
|
private syncPlayEnabledAtPlayer = false;
|
||||||
|
/** Are we mirroring the group's commands locally? */
|
||||||
|
private followingGroupPlayback = true;
|
||||||
|
|
||||||
|
constructor(api: Api) {
|
||||||
|
super();
|
||||||
|
this.apiClient = api;
|
||||||
|
this.playerWrapper = new PlayerWrapper();
|
||||||
|
this.timeSync = new TimeSync(api);
|
||||||
|
this.playbackCore = new PlaybackCore();
|
||||||
|
this.queueCore = new QueueCore();
|
||||||
|
this.pendingPlaybackTracker = new PendingPlaybackTracker();
|
||||||
|
this.controller = new Controller();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Wire up cores. Called once after construction. */
|
||||||
|
init(): void {
|
||||||
|
this.playbackCore.init(this);
|
||||||
|
this.queueCore.init(this);
|
||||||
|
this.controller.init(this);
|
||||||
|
|
||||||
|
// Forward PlaybackCore OSD events to provider listeners.
|
||||||
|
this.playbackCore.on("osd", (...args) => {
|
||||||
|
this.emit("osd", ...args);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bridge optimistic pending Pause/Unpause → React state.
|
||||||
|
this.pendingPlaybackTracker.setChangeHandler((cmd) => {
|
||||||
|
this.emit("pending-playback-change", cmd);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.timeSync.startPing();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Public controller for callers. */
|
||||||
|
getController(): Controller {
|
||||||
|
return this.controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Called by SyncPlayProvider when the user switches Jellyfin servers. */
|
||||||
|
updateApiClient(api: Api): void {
|
||||||
|
this.apiClient = api;
|
||||||
|
this.timeSync.updateApiClient(api);
|
||||||
|
}
|
||||||
|
|
||||||
|
getApiClient(): Api {
|
||||||
|
return this.apiClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlayerWrapper(): PlayerWrapper {
|
||||||
|
return this.playerWrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTimeSync(): TimeSync {
|
||||||
|
return this.timeSync;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlaybackCore(): PlaybackCore {
|
||||||
|
return this.playbackCore;
|
||||||
|
}
|
||||||
|
|
||||||
|
getQueueCore(): QueueCore {
|
||||||
|
return this.queueCore;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPendingPlaybackTracker(): PendingPlaybackTracker {
|
||||||
|
return this.pendingPlaybackTracker;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// WebSocket message handlers (called by useSyncPlayWebSocket)
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a `SyncPlayGroupUpdate` WebSocket message.
|
||||||
|
*
|
||||||
|
* Cast: the SDK's `GroupUpdate.Type` union is narrower than what the
|
||||||
|
* server actually emits (it omits `SyncPlayIsDisabled`, `GroupUpdate`,
|
||||||
|
* `CreateGroupDenied`, `JoinGroupDenied`). Wire format is the source
|
||||||
|
* of truth here.
|
||||||
|
*/
|
||||||
|
processGroupUpdate(rawUpdate: GroupUpdate): void {
|
||||||
|
if (!rawUpdate) {
|
||||||
|
console.warn("SyncPlay processGroupUpdate: empty update");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const update = rawUpdate as unknown as {
|
||||||
|
Type: string;
|
||||||
|
Data: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (update.Type) {
|
||||||
|
case "PlayQueue":
|
||||||
|
this.queueCore.updatePlayQueue(
|
||||||
|
this.apiClient,
|
||||||
|
update.Data as unknown as PlayQueueUpdate,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "UserJoined":
|
||||||
|
case "UserLeft":
|
||||||
|
// Group membership notifications — current group will follow
|
||||||
|
// via GroupUpdate, but emit a toast for friendliness.
|
||||||
|
this.emit("toast", `MessageSyncPlay${update.Type}`, update.Data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "GroupJoined": {
|
||||||
|
this.groupInfo = update.Data as GroupInfoDto;
|
||||||
|
this.enableSyncPlay(this.groupInfo);
|
||||||
|
this.emit("group-update", this.groupInfo);
|
||||||
|
this.emit("toast", "MessageSyncPlayGroupJoined");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "GroupLeft":
|
||||||
|
case "NotInGroup":
|
||||||
|
case "SyncPlayIsDisabled": {
|
||||||
|
const previousState = this.groupInfo?.State;
|
||||||
|
this.groupInfo = null;
|
||||||
|
this.disableSyncPlay();
|
||||||
|
this.emit("group-update", null);
|
||||||
|
if (update.Type === "GroupLeft") {
|
||||||
|
this.emit("toast", "MessageSyncPlayGroupLeft");
|
||||||
|
}
|
||||||
|
if (previousState) {
|
||||||
|
this.emit("group-state-change", "Idle", previousState);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "GroupUpdate": {
|
||||||
|
const previousState = this.groupInfo?.State;
|
||||||
|
this.groupInfo = update.Data as GroupInfoDto;
|
||||||
|
this.emit("group-update", this.groupInfo);
|
||||||
|
const newState = this.groupInfo.State;
|
||||||
|
if (newState && newState !== previousState) {
|
||||||
|
this.emit("group-state-change", newState, previousState ?? "Idle");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "StateUpdate": {
|
||||||
|
const stateData = update.Data as {
|
||||||
|
State?: string;
|
||||||
|
PreviousState?: string;
|
||||||
|
Reason?: string;
|
||||||
|
};
|
||||||
|
const newState = stateData.State ?? "Idle";
|
||||||
|
const previousState = stateData.PreviousState ?? "Idle";
|
||||||
|
const reason = stateData.Reason;
|
||||||
|
if (this.groupInfo) {
|
||||||
|
this.groupInfo.State = newState as GroupInfoDto["State"];
|
||||||
|
this.emit("group-update", this.groupInfo);
|
||||||
|
}
|
||||||
|
this.emit("group-state-change", newState, previousState, reason);
|
||||||
|
// Server signals "Playing" or "Paused" → clear any in-flight
|
||||||
|
// optimistic tap state.
|
||||||
|
if (newState === "Playing" || newState === "Paused") {
|
||||||
|
this.pendingPlaybackTracker.clear();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "CreateGroupDenied":
|
||||||
|
this.emit("toast", "MessageSyncPlayCreateGroupDenied");
|
||||||
|
break;
|
||||||
|
case "JoinGroupDenied":
|
||||||
|
this.emit("toast", "MessageSyncPlayJoinGroupDenied");
|
||||||
|
break;
|
||||||
|
case "LibraryAccessDenied":
|
||||||
|
this.emit("toast", "MessageSyncPlayLibraryAccessDenied");
|
||||||
|
break;
|
||||||
|
case "GroupDoesNotExist":
|
||||||
|
this.emit("toast", "MessageSyncPlayGroupDoesNotExist");
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.warn("SyncPlay processGroupUpdate: unknown type", update.Type);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Handle a `SyncPlayCommand` WebSocket message. */
|
||||||
|
processCommand(command: SendCommand): void {
|
||||||
|
if (!command) {
|
||||||
|
console.warn("SyncPlay processCommand: empty command");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.playbackCore.applyCommand(command);
|
||||||
|
// Server told us the new playing state — clear optimistic UI.
|
||||||
|
if (command.Command === "Unpause" || command.Command === "Pause") {
|
||||||
|
this.pendingPlaybackTracker.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Enable / disable SyncPlay
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
private enableSyncPlay(_group: GroupInfoDto): void {
|
||||||
|
if (this.syncPlayEnabledAtPlayer) return;
|
||||||
|
this.syncPlayEnabledAtPlayer = true;
|
||||||
|
this.followingGroupPlayback = true;
|
||||||
|
this.timeSync.forceUpdate();
|
||||||
|
this.emit("enabled", true);
|
||||||
|
this.emit("play-state-change", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private disableSyncPlay(): void {
|
||||||
|
if (!this.syncPlayEnabledAtPlayer) return;
|
||||||
|
this.syncPlayEnabledAtPlayer = false;
|
||||||
|
this.followingGroupPlayback = false;
|
||||||
|
this.playbackCore.clearScheduledCommand();
|
||||||
|
this.queueCore.clear();
|
||||||
|
this.pendingPlaybackTracker.clear();
|
||||||
|
this.emit("enabled", false);
|
||||||
|
this.emit("play-state-change", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resume following group playback after the user temporarily took
|
||||||
|
* local control (e.g. scrubbed the seek bar).
|
||||||
|
*/
|
||||||
|
async followGroupPlayback(_api: Api): Promise<void> {
|
||||||
|
this.followingGroupPlayback = true;
|
||||||
|
this.emit("play-state-change", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stop following group playback (e.g., user takes local control). */
|
||||||
|
haltGroupPlayback(_api: Api): void {
|
||||||
|
this.followingGroupPlayback = false;
|
||||||
|
this.emit("play-state-change", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
isFollowingGroupPlayback(): boolean {
|
||||||
|
return this.followingGroupPlayback;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSyncPlayEnabled(): boolean {
|
||||||
|
return this.syncPlayEnabledAtPlayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Player attach + provider bridges
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind the RN player controls.
|
||||||
|
* Called from the player screen's `useEffect`. Triggers a reconcile
|
||||||
|
* if a group is active and the player is late-arriving.
|
||||||
|
*/
|
||||||
|
setPlayerControls(controls: PlayerControls | null): void {
|
||||||
|
this.playerWrapper.bindToControls(controls);
|
||||||
|
if (controls && this.syncPlayEnabledAtPlayer) {
|
||||||
|
const lastCommand = this.playbackCore.getLastCommand();
|
||||||
|
reconcileToGroupOnAttach(controls, lastCommand, (local) =>
|
||||||
|
this.timeSync.localDateToRemote(local),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Player-side notify hook: media is ready to play. */
|
||||||
|
notifyReady(): void {
|
||||||
|
this.emit("playbackstart");
|
||||||
|
if (this.syncPlayEnabledAtPlayer) {
|
||||||
|
this.playbackCore.onReady(this.apiClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Player-side notify hook: buffering state changed. */
|
||||||
|
notifyBuffering(isBuffering: boolean): void {
|
||||||
|
if (!this.syncPlayEnabledAtPlayer) return;
|
||||||
|
if (isBuffering) {
|
||||||
|
this.playbackCore.onBuffering(this.apiClient);
|
||||||
|
} else {
|
||||||
|
this.playbackCore.onReady(this.apiClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Player-side notify hook: local playback started. */
|
||||||
|
notifyPlaybackStart(): void {
|
||||||
|
this.emit("playbackstart");
|
||||||
|
if (this.syncPlayEnabledAtPlayer) {
|
||||||
|
this.playbackCore.onPlaybackStart(this.apiClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Pending playback (optimistic UI for play/pause taps)
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/** Called by Controller before sending an Unpause/Pause request. */
|
||||||
|
markPendingPlaybackCommand(command: "Unpause" | "Pause"): void {
|
||||||
|
this.pendingPlaybackTracker.mark(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Is the group currently playing? Used by Controller.playPause. */
|
||||||
|
isPlaying(): boolean {
|
||||||
|
const pending = this.pendingPlaybackTracker.get();
|
||||||
|
if (pending === "Unpause") return true;
|
||||||
|
if (pending === "Pause") return false;
|
||||||
|
return this.groupInfo?.State === "Playing";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Group info for consumers. */
|
||||||
|
getGroupInfo(): GroupInfoDto | null {
|
||||||
|
return this.groupInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Last playback command (for QueueCore.startPlayback resumption). */
|
||||||
|
getLastPlaybackCommand(): SendCommand | null {
|
||||||
|
return this.playbackCore.getLastCommand();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Teardown
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this.timeSync.destroy();
|
||||||
|
this.playbackCore.destroy();
|
||||||
|
this.queueCore.destroy();
|
||||||
|
this.playerWrapper.bindToControls(null);
|
||||||
|
this.removeAllListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SyncPlayManager;
|
||||||
527
providers/SyncPlay/SyncPlayProvider.tsx
Normal file
527
providers/SyncPlay/SyncPlayProvider.tsx
Normal file
@@ -0,0 +1,527 @@
|
|||||||
|
/**
|
||||||
|
* SyncPlayProvider — React glue around `SyncPlayManager`.
|
||||||
|
*
|
||||||
|
* Responsibilities:
|
||||||
|
* - Manager lifecycle (construct on api change, destroy on unmount)
|
||||||
|
* - React mirrors of manager state (`isEnabled`, `groupInfo`,
|
||||||
|
* `pendingPlaybackCommand`) so components re-render
|
||||||
|
* - Navigation handlers wired into `PlayerWrapper.localPlay` /
|
||||||
|
* `localSetCurrentPlaylistItem` — these are what jellyfin-web does
|
||||||
|
* synchronously via `playbackManager.play`; on RN they navigate
|
||||||
|
* to the player screen instead
|
||||||
|
* - AppState foreground re-join (we may miss broadcasts while
|
||||||
|
* suspended)
|
||||||
|
*
|
||||||
|
* External API surface (`useSyncPlay`) is stable; components don't
|
||||||
|
* change when the internals do.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getSyncPlayApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
|
import { useAtomValue } from "jotai";
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
type ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { AppState, type AppStateStatus } from "react-native";
|
||||||
|
import { toast } from "sonner-native";
|
||||||
|
import { useAppRouter } from "@/hooks/useAppRouter";
|
||||||
|
import { useKeepWebSocketAlive } from "@/hooks/useKeepWebSocketAlive";
|
||||||
|
import i18n from "@/i18n";
|
||||||
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
|
import { useWebSocketContext } from "@/providers/WebSocketProvider";
|
||||||
|
import type { Controller as SyncPlayController } from "./Controller";
|
||||||
|
import { SyncPlayManager } from "./Manager";
|
||||||
|
import { useSyncPlayWebSocket } from "./transport/useSyncPlayWebSocket";
|
||||||
|
import type { GroupInfoDto, PlayerControls, SyncPlayOsdAction } from "./types";
|
||||||
|
|
||||||
|
interface SyncPlayContextValue {
|
||||||
|
isEnabled: boolean;
|
||||||
|
groupInfo: GroupInfoDto | null;
|
||||||
|
canJoinGroups: boolean;
|
||||||
|
canCreateGroups: boolean;
|
||||||
|
|
||||||
|
joinGroup: (groupId: string) => Promise<void>;
|
||||||
|
createGroup: (groupName?: string) => Promise<void>;
|
||||||
|
leaveGroup: () => Promise<void>;
|
||||||
|
getGroups: () => Promise<GroupInfoDto[]>;
|
||||||
|
|
||||||
|
controller: SyncPlayController | null;
|
||||||
|
|
||||||
|
setPlayerControls: (controls: PlayerControls | null) => void;
|
||||||
|
notifyReady: () => void;
|
||||||
|
notifyBuffering: (isBuffering: boolean) => void;
|
||||||
|
notifyPlaybackStart: () => void;
|
||||||
|
|
||||||
|
pendingPlaybackCommand: "Unpause" | "Pause" | null;
|
||||||
|
/**
|
||||||
|
* Current SyncPlay OSD overlay state. Drives the animated icon over the
|
||||||
|
* video that mirrors jellyfin-web's `#syncPlayIcon`. `null` means hidden.
|
||||||
|
*/
|
||||||
|
osdAction: SyncPlayOsdAction | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SyncPlayContext = createContext<SyncPlayContextValue | null>(null);
|
||||||
|
|
||||||
|
interface SyncPlayProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SyncPlayProvider({ children }: SyncPlayProviderProps) {
|
||||||
|
const api = useAtomValue(apiAtom);
|
||||||
|
const user = useAtomValue(userAtom);
|
||||||
|
const router = useAppRouter();
|
||||||
|
const { isConnected: isWsConnected } = useWebSocketContext();
|
||||||
|
|
||||||
|
const [manager, setManager] = useState<SyncPlayManager | null>(null);
|
||||||
|
const isNavigatingToPlayerRef = useRef(false);
|
||||||
|
|
||||||
|
const [isEnabled, setIsEnabled] = useState(false);
|
||||||
|
const [groupInfo, setGroupInfo] = useState<GroupInfoDto | null>(null);
|
||||||
|
const [pendingPlaybackCommand, setPendingPlaybackCommand] = useState<
|
||||||
|
"Unpause" | "Pause" | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
|
// While in a SyncPlay group, hold a keep-alive token on the global
|
||||||
|
// WebSocket so backgrounding the app does NOT cleanly close the
|
||||||
|
// socket. A clean close is interpreted by the Jellyfin server as
|
||||||
|
// leaving the group and is broadcast to every other member as
|
||||||
|
// "<user> has left the group". Keeping the socket open across a
|
||||||
|
// short suspend lets us stay in the group while quickly switching
|
||||||
|
// apps; if the OS eventually tears the TCP connection down anyway,
|
||||||
|
// the app-foreground rejoin effect below will pull us back in.
|
||||||
|
useKeepWebSocketAlive(isEnabled);
|
||||||
|
|
||||||
|
const [osdAction, setOsdAction] = useState<SyncPlayOsdAction | null>(null);
|
||||||
|
const osdTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the OSD overlay action.
|
||||||
|
*
|
||||||
|
* `transient` mirrors jellyfin-web's `iconVisibilityTime = 1500` for the
|
||||||
|
* Unpause / Pause / Seek command-confirmation flashes. Persistent actions
|
||||||
|
* (schedule-play, buffering, wait-*) stay until cleared by a state
|
||||||
|
* transition or a subsequent call with `null`.
|
||||||
|
*/
|
||||||
|
const showOsd = useCallback(
|
||||||
|
(action: SyncPlayOsdAction | null, transient = false) => {
|
||||||
|
if (osdTimeoutRef.current) {
|
||||||
|
clearTimeout(osdTimeoutRef.current);
|
||||||
|
osdTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
setOsdAction(action);
|
||||||
|
if (transient && action !== null) {
|
||||||
|
osdTimeoutRef.current = setTimeout(() => {
|
||||||
|
osdTimeoutRef.current = null;
|
||||||
|
setOsdAction((cur) => (cur === action ? null : cur));
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pending play/pause tap → optimistic schedule-play overlay (unless another
|
||||||
|
// overlay reason has already taken precedence).
|
||||||
|
useEffect(() => {
|
||||||
|
if (pendingPlaybackCommand) {
|
||||||
|
setOsdAction((cur) => cur ?? "schedule-play");
|
||||||
|
} else {
|
||||||
|
setOsdAction((cur) => (cur === "schedule-play" ? null : cur));
|
||||||
|
}
|
||||||
|
}, [pendingPlaybackCommand]);
|
||||||
|
|
||||||
|
// Clear the OSD auto-expire timeout on unmount.
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (osdTimeoutRef.current) {
|
||||||
|
clearTimeout(osdTimeoutRef.current);
|
||||||
|
osdTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const canJoinGroups = useMemo(() => {
|
||||||
|
const access = user?.Policy?.SyncPlayAccess;
|
||||||
|
return access !== "None" && access !== undefined;
|
||||||
|
}, [user?.Policy?.SyncPlayAccess]);
|
||||||
|
|
||||||
|
const canCreateGroups = useMemo(
|
||||||
|
() => user?.Policy?.SyncPlayAccess === "CreateAndJoinGroups",
|
||||||
|
[user?.Policy?.SyncPlayAccess],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Latch: `true` once we've fired the per-attach `playbackstart` event.
|
||||||
|
const playbackStartFiredRef = useRef(false);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Manager lifecycle
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!api) return;
|
||||||
|
|
||||||
|
const mgr = new SyncPlayManager(api);
|
||||||
|
mgr.init();
|
||||||
|
setManager(mgr);
|
||||||
|
|
||||||
|
const playerWrapper = mgr.getPlayerWrapper();
|
||||||
|
|
||||||
|
// localPlay → navigate to direct-player with syncPlay=true
|
||||||
|
playerWrapper.setLocalPlayHandler((options) => {
|
||||||
|
const itemId = options.ids[0];
|
||||||
|
if (!itemId) {
|
||||||
|
console.warn("SyncPlay: localPlay called with no ids");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isNavigatingToPlayerRef.current) {
|
||||||
|
console.debug("SyncPlay: already navigating to player");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isNavigatingToPlayerRef.current = true;
|
||||||
|
|
||||||
|
toast(i18n.t("syncplay.joining_playback"));
|
||||||
|
|
||||||
|
const queryParams = new URLSearchParams({
|
||||||
|
itemId,
|
||||||
|
playbackPosition: String(options.startPositionTicks ?? 0),
|
||||||
|
syncPlay: "true",
|
||||||
|
}).toString();
|
||||||
|
router.push(`/player/direct-player?${queryParams}`);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
isNavigatingToPlayerRef.current = false;
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// localSetCurrentPlaylistItem → navigate to the new playlist item
|
||||||
|
playerWrapper.setLocalSetCurrentItemHandler((playlistItemId) => {
|
||||||
|
if (!playlistItemId) return;
|
||||||
|
const queueCore = mgr.getQueueCore();
|
||||||
|
const target = queueCore
|
||||||
|
.getPlaylist()
|
||||||
|
.find((i) => i.PlaylistItemId === playlistItemId);
|
||||||
|
const itemId = target?.Id;
|
||||||
|
if (!itemId) {
|
||||||
|
console.warn(
|
||||||
|
"SyncPlay: localSetCurrentPlaylistItem — item not in playlist",
|
||||||
|
playlistItemId,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isNavigatingToPlayerRef.current) return;
|
||||||
|
isNavigatingToPlayerRef.current = true;
|
||||||
|
|
||||||
|
const queryParams = new URLSearchParams({
|
||||||
|
itemId,
|
||||||
|
playbackPosition: String(queueCore.getStartPositionTicks()),
|
||||||
|
syncPlay: "true",
|
||||||
|
}).toString();
|
||||||
|
router.push(`/player/direct-player?${queryParams}`);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
isNavigatingToPlayerRef.current = false;
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
mgr.on("enabled", (...args: unknown[]) => {
|
||||||
|
const enabled = args[0] as boolean;
|
||||||
|
setIsEnabled(enabled);
|
||||||
|
if (!enabled) setGroupInfo(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
mgr.on("group-update", (...args: unknown[]) => {
|
||||||
|
setGroupInfo((args[0] as GroupInfoDto | null | undefined) ?? null);
|
||||||
|
});
|
||||||
|
|
||||||
|
mgr.on("pending-playback-change", (...args: unknown[]) => {
|
||||||
|
setPendingPlaybackCommand(args[0] as "Unpause" | "Pause" | null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// group-state-change → on "Waiting", pause locally so we don't drift
|
||||||
|
// ahead of the group while the server is reconciling buffering/seek
|
||||||
|
// state. Position resync is *only* done from the explicit Pause /
|
||||||
|
// Unpause / Seek SendCommands that follow (`PlaybackCore.applyCommand`
|
||||||
|
// → `scheduleUnpause` etc.) — those commands carry the canonical
|
||||||
|
// `PositionTicks` for the action's `When`. The old code here also
|
||||||
|
// called `wrapper.localSeek(lastCommand.PositionTicks)`, but
|
||||||
|
// `lastCommand` is the *previous* Pause/Unpause and can be many
|
||||||
|
// seconds stale, so it rewound the user every time someone else
|
||||||
|
// buffered. Don't put a seek back here.
|
||||||
|
mgr.on("group-state-change", (...args: unknown[]) => {
|
||||||
|
const state = args[0] as string | undefined;
|
||||||
|
const reason = args[2] as string | undefined;
|
||||||
|
const wrapper = mgr.getPlayerWrapper();
|
||||||
|
if (!wrapper.isPlaybackActive()) return;
|
||||||
|
if (state === "Waiting") {
|
||||||
|
wrapper.localPause();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drive the persistent OSD overlay from (state, reason).
|
||||||
|
// Mirrors jellyfin-web's `group-state-update` → `showIcon` mapping.
|
||||||
|
if (state === "Waiting") {
|
||||||
|
if (reason === "Buffer") showOsd("buffering");
|
||||||
|
else if (reason === "Unpause") showOsd("wait-unpause");
|
||||||
|
else if (reason === "Pause") showOsd("wait-pause");
|
||||||
|
else if (reason === "Seek") showOsd("seek");
|
||||||
|
} else if (state === "Playing" || state === "Paused") {
|
||||||
|
// Stable state — clear any persistent overlay; transient flashes
|
||||||
|
// come from the `osd` event below and self-expire.
|
||||||
|
setOsdAction((cur) => {
|
||||||
|
if (
|
||||||
|
cur === "schedule-play" ||
|
||||||
|
cur === "buffering" ||
|
||||||
|
cur === "wait-pause" ||
|
||||||
|
cur === "wait-unpause"
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return cur;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// PlaybackCore-emitted OSD events. Transient ones auto-expire in 1.5s.
|
||||||
|
mgr.on("osd", (...args: unknown[]) => {
|
||||||
|
const action = args[0] as SyncPlayOsdAction;
|
||||||
|
const transient =
|
||||||
|
action === "unpause" || action === "pause" || action === "seek";
|
||||||
|
showOsd(action, transient);
|
||||||
|
});
|
||||||
|
|
||||||
|
mgr.on("toast", (...args: unknown[]) => {
|
||||||
|
const key = args[0] as string;
|
||||||
|
const arg = args[1] as string | undefined;
|
||||||
|
const message = arg
|
||||||
|
? i18n.t(`syncplay.toasts.${key}`, { user: arg })
|
||||||
|
: i18n.t(`syncplay.toasts.${key}`);
|
||||||
|
toast(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mgr.destroy();
|
||||||
|
setManager(null);
|
||||||
|
};
|
||||||
|
}, [api, router]);
|
||||||
|
|
||||||
|
// Initial join race: once `enabled` flips true, snapshot the current group.
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEnabled && manager) {
|
||||||
|
setGroupInfo(manager.getGroupInfo());
|
||||||
|
}
|
||||||
|
}, [isEnabled, manager]);
|
||||||
|
|
||||||
|
// Wire WebSocket messages → manager
|
||||||
|
useSyncPlayWebSocket(manager);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Group management
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const getGroups = useCallback(async (): Promise<GroupInfoDto[]> => {
|
||||||
|
if (!api) return [];
|
||||||
|
try {
|
||||||
|
const response = await getSyncPlayApi(api).syncPlayGetGroups();
|
||||||
|
return (response.data as unknown as GroupInfoDto[]) ?? [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error("SyncPlay: failed to get groups", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}, [api]);
|
||||||
|
|
||||||
|
const joinGroup = useCallback(
|
||||||
|
async (groupId: string): Promise<void> => {
|
||||||
|
if (!api) return;
|
||||||
|
try {
|
||||||
|
await getSyncPlayApi(api).syncPlayJoinGroup({
|
||||||
|
joinGroupRequestDto: { GroupId: groupId },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("SyncPlay: failed to join group", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[api],
|
||||||
|
);
|
||||||
|
|
||||||
|
const createGroup = useCallback(
|
||||||
|
async (groupName?: string): Promise<void> => {
|
||||||
|
if (!api || !user) return;
|
||||||
|
const name = groupName || `${user.Name}'s Group`;
|
||||||
|
try {
|
||||||
|
await getSyncPlayApi(api).syncPlayCreateGroup({
|
||||||
|
newGroupRequestDto: { GroupName: name },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("SyncPlay: failed to create group", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[api, user],
|
||||||
|
);
|
||||||
|
|
||||||
|
const leaveGroup = useCallback(async (): Promise<void> => {
|
||||||
|
if (!api) return;
|
||||||
|
try {
|
||||||
|
await getSyncPlayApi(api).syncPlayLeaveGroup();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("SyncPlay: failed to leave group", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}, [api]);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// App foreground re-join (idempotent; gets us a fresh GroupJoined snapshot)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const lastGroupIdRef = useRef<string | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
lastGroupIdRef.current = groupInfo?.GroupId ?? null;
|
||||||
|
}, [groupInfo?.GroupId]);
|
||||||
|
|
||||||
|
// Track whether the WebSocket got torn down while the app was
|
||||||
|
// backgrounded. If it survived (keep-alive worked), the server
|
||||||
|
// still has us in the group and we must NOT call JoinGroup again —
|
||||||
|
// doing so would trigger a redundant "X joined the group" broadcast
|
||||||
|
// to every other member every time we briefly leave the app.
|
||||||
|
const wsClosedWhileBackgroundedRef = useRef(false);
|
||||||
|
const appStateRef = useRef<AppStateStatus>(AppState.currentState);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isWsConnected && appStateRef.current !== "active") {
|
||||||
|
wsClosedWhileBackgroundedRef.current = true;
|
||||||
|
}
|
||||||
|
}, [isWsConnected]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!api) return;
|
||||||
|
|
||||||
|
const subscription = AppState.addEventListener("change", (nextAppState) => {
|
||||||
|
const previousAppState = appStateRef.current;
|
||||||
|
appStateRef.current = nextAppState;
|
||||||
|
|
||||||
|
const becameActive =
|
||||||
|
(previousAppState === "background" ||
|
||||||
|
previousAppState === "inactive") &&
|
||||||
|
nextAppState === "active";
|
||||||
|
if (!becameActive) return;
|
||||||
|
|
||||||
|
const groupId = lastGroupIdRef.current;
|
||||||
|
if (!groupId) return;
|
||||||
|
|
||||||
|
// Happy path: keep-alive held the socket open across the
|
||||||
|
// suspend. Server still considers us a member — nothing to do.
|
||||||
|
if (!wsClosedWhileBackgroundedRef.current) {
|
||||||
|
console.log(
|
||||||
|
"SyncPlay: app foregrounded with WS still alive, skipping rejoin",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
wsClosedWhileBackgroundedRef.current = false;
|
||||||
|
|
||||||
|
// Small delay so the WebSocket has a moment to reconnect.
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log(
|
||||||
|
`SyncPlay: app foregrounded after WS drop, rejoining group ${groupId}`,
|
||||||
|
);
|
||||||
|
getSyncPlayApi(api)
|
||||||
|
.syncPlayJoinGroup({ joinGroupRequestDto: { GroupId: groupId } })
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("SyncPlay: failed to rejoin group", error);
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => subscription.remove();
|
||||||
|
}, [api]);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Player attach bridges
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const setPlayerControls = useCallback(
|
||||||
|
(controls: PlayerControls | null) => {
|
||||||
|
// Reset the playbackstart latch on each new attach.
|
||||||
|
playbackStartFiredRef.current = false;
|
||||||
|
manager?.setPlayerControls(controls);
|
||||||
|
},
|
||||||
|
[manager],
|
||||||
|
);
|
||||||
|
|
||||||
|
const notifyReady = useCallback(() => {
|
||||||
|
manager?.notifyReady();
|
||||||
|
}, [manager]);
|
||||||
|
|
||||||
|
const notifyBuffering = useCallback(
|
||||||
|
(isBuffering: boolean) => {
|
||||||
|
manager?.notifyBuffering(isBuffering);
|
||||||
|
if (!isBuffering && !playbackStartFiredRef.current) {
|
||||||
|
playbackStartFiredRef.current = true;
|
||||||
|
manager?.notifyPlaybackStart();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[manager],
|
||||||
|
);
|
||||||
|
|
||||||
|
const notifyPlaybackStart = useCallback(() => {
|
||||||
|
manager?.notifyPlaybackStart();
|
||||||
|
}, [manager]);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Context value
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const contextValue: SyncPlayContextValue = useMemo(
|
||||||
|
() => ({
|
||||||
|
isEnabled,
|
||||||
|
groupInfo,
|
||||||
|
canJoinGroups,
|
||||||
|
canCreateGroups,
|
||||||
|
joinGroup,
|
||||||
|
createGroup,
|
||||||
|
leaveGroup,
|
||||||
|
getGroups,
|
||||||
|
controller: manager?.getController() ?? null,
|
||||||
|
setPlayerControls,
|
||||||
|
notifyReady,
|
||||||
|
notifyBuffering,
|
||||||
|
notifyPlaybackStart,
|
||||||
|
pendingPlaybackCommand,
|
||||||
|
osdAction,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
isEnabled,
|
||||||
|
groupInfo,
|
||||||
|
canJoinGroups,
|
||||||
|
canCreateGroups,
|
||||||
|
joinGroup,
|
||||||
|
createGroup,
|
||||||
|
leaveGroup,
|
||||||
|
getGroups,
|
||||||
|
manager,
|
||||||
|
setPlayerControls,
|
||||||
|
notifyReady,
|
||||||
|
notifyBuffering,
|
||||||
|
notifyPlaybackStart,
|
||||||
|
pendingPlaybackCommand,
|
||||||
|
osdAction,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SyncPlayContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
</SyncPlayContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSyncPlay(): SyncPlayContextValue {
|
||||||
|
const context = useContext(SyncPlayContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useSyncPlay must be used within a SyncPlayProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
23
providers/SyncPlay/constants.ts
Normal file
23
providers/SyncPlay/constants.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Constants — shared timing/threshold values used across SyncPlay files.
|
||||||
|
* Kept separate from `types.ts` because these are implementation tuning
|
||||||
|
* values, not the public protocol/types surface.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { TicksPerMillisecond } from "./types";
|
||||||
|
|
||||||
|
export { TicksPerMillisecond };
|
||||||
|
|
||||||
|
/** Default timeout for `waitForEventOnce` (matches jellyfin-web). */
|
||||||
|
export const WaitForEventDefaultTimeout = 30000;
|
||||||
|
|
||||||
|
/** Short-lived timeout for player events (matches jellyfin-web). */
|
||||||
|
export const WaitForPlayerEventTimeout = 500;
|
||||||
|
|
||||||
|
export function ticksToMs(ticks: number): number {
|
||||||
|
return ticks / TicksPerMillisecond;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function msToTicks(ms: number): number {
|
||||||
|
return Math.round(ms * TicksPerMillisecond);
|
||||||
|
}
|
||||||
378
providers/SyncPlay/cores/PlaybackCore.ts
Normal file
378
providers/SyncPlay/cores/PlaybackCore.ts
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
/**
|
||||||
|
* SyncPlay PlaybackCore — schedules unpauses/pauses/seeks/stops to fire
|
||||||
|
* at the precise group-wide moment and keeps the player drift-corrected.
|
||||||
|
*
|
||||||
|
* Design choices that diverge from jellyfin-web:
|
||||||
|
* - **No SpeedToSync**. Our RN players' `setPlaybackSpeed` is unreliable
|
||||||
|
* across platforms (mpv/VLC/expo-video each behave differently for
|
||||||
|
* fractional speeds). We always seek to catch up.
|
||||||
|
* - **No `MessageSyncPlayDuplicateMedia` detection**. Web's detection
|
||||||
|
* used HTML element identity; on RN we don't have a stable handle
|
||||||
|
* and the false-positive rate would be much higher than the value.
|
||||||
|
* - **No syncMethod / showSyncIcon**. We don't surface the sync
|
||||||
|
* technique to the UI.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Api } from "@jellyfin/sdk";
|
||||||
|
import { getSyncPlayApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
|
import {
|
||||||
|
TicksPerMillisecond,
|
||||||
|
ticksToMs,
|
||||||
|
WaitForPlayerEventTimeout,
|
||||||
|
} from "../constants";
|
||||||
|
import { EventEmitter, waitForEventOnce } from "../EventEmitter";
|
||||||
|
import type { SyncPlayManager } from "../Manager";
|
||||||
|
import { type SendCommand, SYNC_PLAY_TUNING } from "../types";
|
||||||
|
|
||||||
|
export class PlaybackCore extends EventEmitter {
|
||||||
|
private manager!: SyncPlayManager;
|
||||||
|
private lastCommand: SendCommand | null = null;
|
||||||
|
private scheduledCommand: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
init(manager: SyncPlayManager): void {
|
||||||
|
this.manager = manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Local "playback started" hook — fires the initial Ready request. */
|
||||||
|
onPlaybackStart(apiClient: Api): void {
|
||||||
|
try {
|
||||||
|
const playerWrapper = this.manager.getPlayerWrapper();
|
||||||
|
const positionMs = playerWrapper.currentTime();
|
||||||
|
const positionTicks = Math.round(positionMs * TicksPerMillisecond);
|
||||||
|
const isPlaying = playerWrapper.isPlaying();
|
||||||
|
const playlistItemId =
|
||||||
|
this.manager.getQueueCore().getCurrentPlaylistItemId() ?? undefined;
|
||||||
|
const now = this.manager.getTimeSync().localDateToRemote(new Date());
|
||||||
|
|
||||||
|
getSyncPlayApi(apiClient).syncPlayReady({
|
||||||
|
readyRequestDto: {
|
||||||
|
When: now.toISOString(),
|
||||||
|
PositionTicks: positionTicks,
|
||||||
|
IsPlaying: isPlaying,
|
||||||
|
PlaylistItemId: playlistItemId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("SyncPlay onPlaybackStart:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Local pause → tell the server. */
|
||||||
|
onPause(apiClient: Api): void {
|
||||||
|
try {
|
||||||
|
getSyncPlayApi(apiClient).syncPlayPause();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("SyncPlay onPause:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Local unpause → tell the server. */
|
||||||
|
onUnpause(apiClient: Api): void {
|
||||||
|
try {
|
||||||
|
getSyncPlayApi(apiClient).syncPlayUnpause();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("SyncPlay onUnpause:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Local "ready" hook — server uses this to know we've finished buffering. */
|
||||||
|
onReady(apiClient: Api): void {
|
||||||
|
this.sendBufferingRequest(apiClient, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Local "buffering" hook — server uses this to (optionally) pause the group. */
|
||||||
|
onBuffering(apiClient: Api): void {
|
||||||
|
this.sendBufferingRequest(apiClient, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Send a Ready or Buffering request. */
|
||||||
|
sendBufferingRequest(apiClient: Api, isBuffering: boolean): void {
|
||||||
|
const playerWrapper = this.manager.getPlayerWrapper();
|
||||||
|
const positionMs = playerWrapper.currentTime();
|
||||||
|
const positionTicks = Math.round(positionMs * TicksPerMillisecond);
|
||||||
|
const isPlaying = playerWrapper.isPlaying();
|
||||||
|
const playlistItemId =
|
||||||
|
this.manager.getQueueCore().getCurrentPlaylistItemId() ?? undefined;
|
||||||
|
const now = this.manager.getTimeSync().localDateToRemote(new Date());
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isBuffering) {
|
||||||
|
getSyncPlayApi(apiClient).syncPlayBuffering({
|
||||||
|
bufferRequestDto: {
|
||||||
|
When: now.toISOString(),
|
||||||
|
PositionTicks: positionTicks,
|
||||||
|
IsPlaying: isPlaying,
|
||||||
|
PlaylistItemId: playlistItemId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
getSyncPlayApi(apiClient).syncPlayReady({
|
||||||
|
readyRequestDto: {
|
||||||
|
When: now.toISOString(),
|
||||||
|
PositionTicks: positionTicks,
|
||||||
|
IsPlaying: isPlaying,
|
||||||
|
PlaylistItemId: playlistItemId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("SyncPlay sendBufferingRequest:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a group command (Unpause, Pause, Stop, Seek). Times the
|
||||||
|
* execution to fire at the group-wide instant the server selected.
|
||||||
|
*/
|
||||||
|
applyCommand(command: SendCommand): void {
|
||||||
|
(command as unknown as { EmittedAt: Date }).EmittedAt = new Date(
|
||||||
|
command.EmittedAt as unknown as string,
|
||||||
|
);
|
||||||
|
(command as unknown as { When: Date }).When = new Date(
|
||||||
|
command.When as unknown as string,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.lastCommand &&
|
||||||
|
((
|
||||||
|
this.lastCommand as unknown as { EmittedAt: Date }
|
||||||
|
).EmittedAt.getTime() >
|
||||||
|
(command as unknown as { EmittedAt: Date }).EmittedAt.getTime() ||
|
||||||
|
(this.lastCommand as unknown as { When: Date }).When.getTime() >
|
||||||
|
(command as unknown as { When: Date }).When.getTime())
|
||||||
|
) {
|
||||||
|
// NOTE: strict `>` (not `>=`) on EmittedAt — Jellyfin's server timestamps
|
||||||
|
// commands at sub-ms precision but JS `Date` truncates to ms, so two
|
||||||
|
// commands emitted within the same millisecond would otherwise be
|
||||||
|
// rejected as "outdated" and silently dropped. This produced an
|
||||||
|
// unbreakable pause/unpause loop where every fresh command was
|
||||||
|
// discarded. Matches jellyfin-web's check in
|
||||||
|
// `web/src/plugins/syncPlay/core/Manager.js`.
|
||||||
|
console.debug(
|
||||||
|
"SyncPlay applyCommand: dropping outdated command",
|
||||||
|
command,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastCommand = command;
|
||||||
|
if (!this.manager.isFollowingGroupPlayback()) {
|
||||||
|
console.debug(
|
||||||
|
"SyncPlay applyCommand: dropping command (not following playback)",
|
||||||
|
command,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const playerWrapper = this.manager.getPlayerWrapper();
|
||||||
|
if (!playerWrapper.isPlaybackActive()) {
|
||||||
|
console.debug(
|
||||||
|
"SyncPlay applyCommand: dropping command (playback not active)",
|
||||||
|
command,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enqueuedAt = new Date();
|
||||||
|
const remoteEnqueuedAt = this.manager
|
||||||
|
.getTimeSync()
|
||||||
|
.localDateToRemote(enqueuedAt);
|
||||||
|
const localCommandWhen = this.manager
|
||||||
|
.getTimeSync()
|
||||||
|
.remoteDateToLocal(command.When as unknown as Date);
|
||||||
|
|
||||||
|
switch (command.Command) {
|
||||||
|
case "Unpause":
|
||||||
|
this.scheduleUnpause(localCommandWhen, command.PositionTicks ?? 0);
|
||||||
|
this.emit("osd", "unpause");
|
||||||
|
break;
|
||||||
|
case "Pause":
|
||||||
|
this.schedulePause(localCommandWhen, command.PositionTicks ?? 0);
|
||||||
|
this.emit("osd", "pause");
|
||||||
|
break;
|
||||||
|
case "Stop":
|
||||||
|
this.scheduleStop(localCommandWhen);
|
||||||
|
break;
|
||||||
|
case "Seek":
|
||||||
|
this.scheduleSeek(localCommandWhen, command.PositionTicks ?? 0);
|
||||||
|
this.emit("osd", "seek");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn("SyncPlay applyCommand: unknown command", command);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(command as unknown as { When: Date }).When.getTime() <
|
||||||
|
remoteEnqueuedAt.getTime()
|
||||||
|
) {
|
||||||
|
console.debug(
|
||||||
|
"SyncPlay applyCommand: command was scheduled for the past",
|
||||||
|
command,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pre-Unpause hook: emit a wait OSD while we wait for the moment. */
|
||||||
|
scheduleUnpause(when: Date, positionTicks: number): void {
|
||||||
|
this.clearScheduledCommand();
|
||||||
|
const now = Date.now();
|
||||||
|
const playAtTime = when.getTime();
|
||||||
|
const currentPositionMs = this.manager.getPlayerWrapper().currentTime();
|
||||||
|
const currentPositionTicks = Math.round(
|
||||||
|
currentPositionMs * TicksPerMillisecond,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (playAtTime > now) {
|
||||||
|
// Future: seek now, then play at the right moment.
|
||||||
|
this.localSeek(positionTicks);
|
||||||
|
this.scheduledCommand = setTimeout(() => {
|
||||||
|
this.localUnpause();
|
||||||
|
// After playback resumes, the player position will need a
|
||||||
|
// small bump to land on the group target. waitForPlayerEvent
|
||||||
|
// is best-effort.
|
||||||
|
waitForEventOnce(
|
||||||
|
this.manager,
|
||||||
|
"unpause",
|
||||||
|
WaitForPlayerEventTimeout,
|
||||||
|
).catch(() => undefined);
|
||||||
|
}, playAtTime - now);
|
||||||
|
this.emit("osd", "wait-unpause");
|
||||||
|
} else {
|
||||||
|
// Past: catch up now.
|
||||||
|
const targetMs = ticksToMs(positionTicks);
|
||||||
|
const delayMs = now - playAtTime;
|
||||||
|
this.localSeek(Math.round((targetMs + delayMs) * TicksPerMillisecond));
|
||||||
|
this.localUnpause();
|
||||||
|
void currentPositionTicks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
schedulePause(when: Date, positionTicks: number): void {
|
||||||
|
this.clearScheduledCommand();
|
||||||
|
const now = Date.now();
|
||||||
|
const pauseAtTime = when.getTime();
|
||||||
|
|
||||||
|
const callback = () => {
|
||||||
|
this.localUnpause();
|
||||||
|
this.localSeek(positionTicks);
|
||||||
|
this.localPause();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pauseAtTime > now) {
|
||||||
|
this.scheduledCommand = setTimeout(callback, pauseAtTime - now);
|
||||||
|
this.emit("osd", "wait-pause");
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleStop(when: Date): void {
|
||||||
|
this.clearScheduledCommand();
|
||||||
|
const now = Date.now();
|
||||||
|
const stopAtTime = when.getTime();
|
||||||
|
if (stopAtTime > now) {
|
||||||
|
this.scheduledCommand = setTimeout(() => {
|
||||||
|
this.localStop();
|
||||||
|
}, stopAtTime - now);
|
||||||
|
} else {
|
||||||
|
this.localStop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleSeek(when: Date, positionTicks: number): void {
|
||||||
|
this.applyCommand({
|
||||||
|
...this.lastCommand!,
|
||||||
|
Command: "Pause",
|
||||||
|
PositionTicks: positionTicks,
|
||||||
|
When: when as unknown as string,
|
||||||
|
EmittedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clearScheduledCommand(): void {
|
||||||
|
if (this.scheduledCommand) {
|
||||||
|
clearTimeout(this.scheduledCommand);
|
||||||
|
this.scheduledCommand = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- local player ops ------------------------------------------------------
|
||||||
|
|
||||||
|
localUnpause(): void {
|
||||||
|
this.manager.getPlayerWrapper().localUnpause();
|
||||||
|
}
|
||||||
|
|
||||||
|
localPause(): void {
|
||||||
|
this.manager.getPlayerWrapper().localPause();
|
||||||
|
}
|
||||||
|
|
||||||
|
localSeek(positionTicks: number): void {
|
||||||
|
this.manager.getPlayerWrapper().localSeek(positionTicks);
|
||||||
|
}
|
||||||
|
|
||||||
|
localStop(): void {
|
||||||
|
this.manager.getPlayerWrapper().localStop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- queries ---------------------------------------------------------------
|
||||||
|
|
||||||
|
getLastCommand(): SendCommand | null {
|
||||||
|
return this.lastCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimate where the group should be in ticks, given a known
|
||||||
|
* starting position and the time the position was valid at.
|
||||||
|
*/
|
||||||
|
estimateCurrentTicks(positionTicks: number, when: Date): number {
|
||||||
|
const lastCommand = this.lastCommand;
|
||||||
|
if (!lastCommand) return positionTicks;
|
||||||
|
const remoteNow = this.manager.getTimeSync().localDateToRemote(new Date());
|
||||||
|
const elapsedMs = remoteNow.getTime() - when.getTime();
|
||||||
|
if (lastCommand.Command === "Unpause") {
|
||||||
|
return positionTicks + elapsedMs * TicksPerMillisecond;
|
||||||
|
}
|
||||||
|
return positionTicks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drift correction tick — called on every player time update. Skips
|
||||||
|
* to the group's expected position if drift exceeds the threshold.
|
||||||
|
* SpeedToSync is intentionally not implemented (see file header).
|
||||||
|
*/
|
||||||
|
syncPlaybackTime(): void {
|
||||||
|
const lastCommand = this.lastCommand;
|
||||||
|
if (lastCommand?.Command !== "Unpause") return;
|
||||||
|
|
||||||
|
const playerWrapper = this.manager.getPlayerWrapper();
|
||||||
|
if (!playerWrapper.isPlaying()) return;
|
||||||
|
|
||||||
|
const currentMs = playerWrapper.currentTime();
|
||||||
|
const expectedTicks = this.estimateCurrentTicks(
|
||||||
|
lastCommand.PositionTicks ?? 0,
|
||||||
|
lastCommand.When as unknown as Date,
|
||||||
|
);
|
||||||
|
const expectedMs = ticksToMs(expectedTicks);
|
||||||
|
const driftMs = Math.abs(currentMs - expectedMs);
|
||||||
|
|
||||||
|
if (driftMs > SYNC_PLAY_TUNING.minDelaySkipToSync) {
|
||||||
|
console.log(
|
||||||
|
`SyncPlay syncPlaybackTime: drift ${driftMs.toFixed(
|
||||||
|
0,
|
||||||
|
)}ms exceeds threshold, seeking to ${expectedMs.toFixed(0)}ms`,
|
||||||
|
);
|
||||||
|
this.localSeek(expectedTicks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- teardown --------------------------------------------------------------
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this.clearScheduledCommand();
|
||||||
|
this.lastCommand = null;
|
||||||
|
this.removeAllListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PlaybackCore;
|
||||||
332
providers/SyncPlay/cores/QueueCore.ts
Normal file
332
providers/SyncPlay/cores/QueueCore.ts
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
/**
|
||||||
|
* SyncPlay QueueCore — tracks the group's playlist.
|
||||||
|
*
|
||||||
|
* Responsibilities:
|
||||||
|
* - Handle `PlayQueue` group updates (NewPlaylist, SetCurrentItem,
|
||||||
|
* NextItem, PreviousItem, RemoveItems, etc.)
|
||||||
|
* - Resolve the server's flat list of ItemIds into full `BaseItemDto`s
|
||||||
|
* (with PlaylistItemId glued on for SyncPlay requests)
|
||||||
|
* - Expose `currentPlaylistItemId` — required by every SyncPlay
|
||||||
|
* request (Ready, Buffering, Seek) so the server can ignore stale
|
||||||
|
* ones from before the playlist moved
|
||||||
|
* - On NewPlaylist, ask the server we're ready by sending a Buffering
|
||||||
|
* request after the local player emits `playbackstart`
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Api } from "@jellyfin/sdk";
|
||||||
|
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||||
|
import { getSyncPlayApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
|
import { TicksPerMillisecond, WaitForEventDefaultTimeout } from "../constants";
|
||||||
|
import { EventEmitter, waitForEventOnce } from "../EventEmitter";
|
||||||
|
import type { SyncPlayManager } from "../Manager";
|
||||||
|
import {
|
||||||
|
getItemsForPlayback,
|
||||||
|
translateItemsForPlayback,
|
||||||
|
} from "../transport/queueTranslation";
|
||||||
|
import type {
|
||||||
|
PlayQueueUpdate,
|
||||||
|
PlayQueueUpdateReason,
|
||||||
|
SyncPlayQueueItem,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
|
export class QueueCore extends EventEmitter {
|
||||||
|
private manager!: SyncPlayManager;
|
||||||
|
private lastPlayQueueUpdate: PlayQueueUpdate | null = null;
|
||||||
|
/** Playable items with `PlaylistItemId` glued on. */
|
||||||
|
private playlist: BaseItemDto[] = [];
|
||||||
|
|
||||||
|
init(manager: SyncPlayManager): void {
|
||||||
|
this.manager = manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Handle a PlayQueue group update from the server. */
|
||||||
|
updatePlayQueue(apiClient: Api, newPlayQueue: PlayQueueUpdate): void {
|
||||||
|
(newPlayQueue as unknown as { LastUpdate: Date }).LastUpdate = new Date(
|
||||||
|
newPlayQueue.LastUpdate as unknown as string,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
(newPlayQueue.LastUpdate as unknown as Date).getTime() <=
|
||||||
|
this.getLastUpdateTime()
|
||||||
|
) {
|
||||||
|
console.debug("SyncPlay updatePlayQueue: ignoring old update");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onPlayQueueUpdate(apiClient, newPlayQueue)
|
||||||
|
.then(() => {
|
||||||
|
if (
|
||||||
|
(newPlayQueue.LastUpdate as unknown as Date).getTime() <
|
||||||
|
this.getLastUpdateTime()
|
||||||
|
) {
|
||||||
|
console.warn("SyncPlay updatePlayQueue: trying to apply old update");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reason = newPlayQueue.Reason as PlayQueueUpdateReason;
|
||||||
|
switch (reason) {
|
||||||
|
case "NewPlaylist": {
|
||||||
|
if (!this.manager.isFollowingGroupPlayback()) {
|
||||||
|
this.manager.followGroupPlayback(apiClient).then(() => {
|
||||||
|
this.startPlayback(apiClient);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.startPlayback(apiClient);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "SetCurrentItem":
|
||||||
|
case "NextItem":
|
||||||
|
case "PreviousItem": {
|
||||||
|
const playlistItemId = this.getCurrentPlaylistItemId();
|
||||||
|
this.setCurrentPlaylistItem(apiClient, playlistItemId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "RemoveItems":
|
||||||
|
case "MoveItem":
|
||||||
|
case "Queue":
|
||||||
|
case "QueueNext":
|
||||||
|
case "RepeatMode":
|
||||||
|
case "ShuffleMode":
|
||||||
|
// Video-focused: we don't expose repeat/shuffle/queue mutation
|
||||||
|
// controls in the RN UI yet, so these reasons just update our
|
||||||
|
// local snapshot (already done by onPlayQueueUpdate) without
|
||||||
|
// triggering any local action.
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn(
|
||||||
|
"SyncPlay updatePlayQueue: unknown reason",
|
||||||
|
newPlayQueue.Reason,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.warn("SyncPlay updatePlayQueue:", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Apply a play-queue update to local state. */
|
||||||
|
async onPlayQueueUpdate(
|
||||||
|
apiClient: Api,
|
||||||
|
playQueueUpdate: PlayQueueUpdate,
|
||||||
|
): Promise<void> {
|
||||||
|
const itemIds = (playQueueUpdate.Playlist ?? [])
|
||||||
|
.map((queueItem: SyncPlayQueueItem) => queueItem.ItemId)
|
||||||
|
.filter((id): id is string => typeof id === "string");
|
||||||
|
|
||||||
|
if (!itemIds.length) {
|
||||||
|
this.lastPlayQueueUpdate = playQueueUpdate;
|
||||||
|
this.playlist = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetched = await getItemsForPlayback(apiClient, itemIds);
|
||||||
|
const items = await translateItemsForPlayback(apiClient, fetched, {
|
||||||
|
ids: itemIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.lastPlayQueueUpdate &&
|
||||||
|
(playQueueUpdate.LastUpdate as unknown as Date).getTime() <=
|
||||||
|
this.getLastUpdateTime()
|
||||||
|
) {
|
||||||
|
throw new Error("Trying to apply old update");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Glue PlaylistItemId from the server's playlist entries onto each
|
||||||
|
// resolved item. The server-assigned IDs are what every SyncPlay
|
||||||
|
// request needs to identify the queue slot.
|
||||||
|
const playlistItems = playQueueUpdate.Playlist ?? [];
|
||||||
|
for (let i = 0; i < items.length && i < playlistItems.length; i++) {
|
||||||
|
items[i].PlaylistItemId = playlistItems[i].PlaylistItemId;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastPlayQueueUpdate = playQueueUpdate;
|
||||||
|
this.playlist = items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a Ready request once the local player begins playback. The
|
||||||
|
* server uses this to wait until every member is buffered before
|
||||||
|
* issuing the next Unpause.
|
||||||
|
*
|
||||||
|
* On timeout (player never starts), halt group playback so the rest
|
||||||
|
* of the group can proceed without us.
|
||||||
|
*/
|
||||||
|
scheduleReadyRequestOnPlaybackStart(apiClient: Api, origin: string): void {
|
||||||
|
waitForEventOnce(
|
||||||
|
this.manager,
|
||||||
|
"playbackstart",
|
||||||
|
WaitForEventDefaultTimeout,
|
||||||
|
["playbackerror"],
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
console.debug(
|
||||||
|
"SyncPlay scheduleReadyRequestOnPlaybackStart: notifying server",
|
||||||
|
);
|
||||||
|
const playerWrapper = this.manager.getPlayerWrapper();
|
||||||
|
playerWrapper.localPause();
|
||||||
|
|
||||||
|
const currentPosition = playerWrapper.currentTime();
|
||||||
|
const currentPositionTicks = Math.round(
|
||||||
|
currentPosition * TicksPerMillisecond,
|
||||||
|
);
|
||||||
|
const isPlaying = playerWrapper.isPlaying();
|
||||||
|
const now = this.manager.getTimeSync().localDateToRemote(new Date());
|
||||||
|
|
||||||
|
try {
|
||||||
|
getSyncPlayApi(apiClient).syncPlayReady({
|
||||||
|
readyRequestDto: {
|
||||||
|
When: now.toISOString(),
|
||||||
|
PositionTicks: currentPositionTicks,
|
||||||
|
IsPlaying: isPlaying,
|
||||||
|
PlaylistItemId: this.getCurrentPlaylistItemId() ?? undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("SyncPlay syncPlayReady failed", error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(
|
||||||
|
"Timed out waiting for 'playbackstart' event!",
|
||||||
|
origin,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
if (!this.manager.isSyncPlayEnabled()) {
|
||||||
|
this.manager.emit("toast", "MessageSyncPlayErrorMedia");
|
||||||
|
}
|
||||||
|
this.manager.haltGroupPlayback(apiClient);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Start local playback by navigating to the player screen for the current item. */
|
||||||
|
startPlayback(apiClient: Api): void {
|
||||||
|
if (!this.manager.isFollowingGroupPlayback()) {
|
||||||
|
console.debug("SyncPlay startPlayback: ignoring, not following playback");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isPlaylistEmpty()) {
|
||||||
|
console.debug("SyncPlay startPlayback: empty playlist");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Estimate where to start playback from. Prefer the last playback
|
||||||
|
// command if newer than the queue update (playback ticks change
|
||||||
|
// more often than queue position).
|
||||||
|
const playbackCommand = this.manager.getLastPlaybackCommand();
|
||||||
|
let startPositionTicks = 0;
|
||||||
|
|
||||||
|
if (
|
||||||
|
playbackCommand &&
|
||||||
|
(
|
||||||
|
playbackCommand as unknown as { EmittedAt: Date }
|
||||||
|
).EmittedAt?.getTime() >= this.getLastUpdateTime()
|
||||||
|
) {
|
||||||
|
startPositionTicks = this.manager
|
||||||
|
.getPlaybackCore()
|
||||||
|
.estimateCurrentTicks(
|
||||||
|
playbackCommand.PositionTicks ?? 0,
|
||||||
|
(playbackCommand as unknown as { When: Date }).When,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
startPositionTicks = this.manager
|
||||||
|
.getPlaybackCore()
|
||||||
|
.estimateCurrentTicks(
|
||||||
|
this.getStartPositionTicks(),
|
||||||
|
(this.getLastUpdate() ?? new Date()) as Date,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverId = apiClient.deviceInfo?.id ?? "";
|
||||||
|
|
||||||
|
this.scheduleReadyRequestOnPlaybackStart(apiClient, "startPlayback");
|
||||||
|
|
||||||
|
this.manager
|
||||||
|
.getPlayerWrapper()
|
||||||
|
.localPlay({
|
||||||
|
ids: this.getPlaylistAsItemIds(),
|
||||||
|
startPositionTicks,
|
||||||
|
startIndex: this.getCurrentPlaylistIndex(),
|
||||||
|
serverId,
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
console.error("SyncPlay startPlayback: localPlay failed", error);
|
||||||
|
this.manager.emit("toast", "MessageSyncPlayErrorMedia");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Navigate to a specific item in the queue. */
|
||||||
|
setCurrentPlaylistItem(apiClient: Api, playlistItemId: string | null): void {
|
||||||
|
if (!this.manager.isFollowingGroupPlayback()) {
|
||||||
|
console.debug(
|
||||||
|
"SyncPlay setCurrentPlaylistItem: ignoring, not following playback",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scheduleReadyRequestOnPlaybackStart(
|
||||||
|
apiClient,
|
||||||
|
"setCurrentPlaylistItem",
|
||||||
|
);
|
||||||
|
|
||||||
|
this.manager.getPlayerWrapper().localSetCurrentPlaylistItem(playlistItemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- getters ---------------------------------------------------------------
|
||||||
|
|
||||||
|
getCurrentPlaylistIndex(): number {
|
||||||
|
return this.lastPlayQueueUpdate?.PlayingItemIndex ?? -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentPlaylistItemId(): string | null {
|
||||||
|
if (!this.lastPlayQueueUpdate) return null;
|
||||||
|
const index = this.lastPlayQueueUpdate.PlayingItemIndex ?? -1;
|
||||||
|
if (index === -1) return null;
|
||||||
|
return this.playlist[index]?.PlaylistItemId ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlaylist(): BaseItemDto[] {
|
||||||
|
return this.playlist.slice(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
isPlaylistEmpty(): boolean {
|
||||||
|
return this.playlist.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLastUpdate(): Date | null {
|
||||||
|
if (!this.lastPlayQueueUpdate) return null;
|
||||||
|
return this.lastPlayQueueUpdate.LastUpdate as unknown as Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLastUpdateTime(): number {
|
||||||
|
if (!this.lastPlayQueueUpdate) return 0;
|
||||||
|
return (this.lastPlayQueueUpdate.LastUpdate as unknown as Date).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
getStartPositionTicks(): number {
|
||||||
|
return this.lastPlayQueueUpdate?.StartPositionTicks ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlaylistAsItemIds(): (string | undefined)[] {
|
||||||
|
if (!this.lastPlayQueueUpdate) return [];
|
||||||
|
return (this.lastPlayQueueUpdate.Playlist ?? []).map((q) => q.ItemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- teardown --------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Clear cached playlist. Called on group disable so a re-join starts clean. */
|
||||||
|
clear(): void {
|
||||||
|
this.lastPlayQueueUpdate = null;
|
||||||
|
this.playlist = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this.clear();
|
||||||
|
this.removeAllListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QueueCore;
|
||||||
220
providers/SyncPlay/cores/TimeSync.ts
Normal file
220
providers/SyncPlay/cores/TimeSync.ts
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
/**
|
||||||
|
* TimeSync — NTP-style time synchronisation with the Jellyfin server.
|
||||||
|
*
|
||||||
|
* Merged port of jellyfin-web's `core/timeSync/{TimeSync,TimeSyncServer,
|
||||||
|
* TimeSyncCore}.js` — three classes that exist on web because the
|
||||||
|
* abstract layer supports syncing against other group members, not just
|
||||||
|
* the server. RN only syncs against the server, so it's one class.
|
||||||
|
*
|
||||||
|
* Algorithm: repeatedly time a round-trip request to `getUtcTime`,
|
||||||
|
* compute `offset = ((requestReceived - requestSent) + (responseSent -
|
||||||
|
* responseReceived)) / 2`, keep the minimum-delay measurement out of
|
||||||
|
* the last 8. This is the standard NTP outlier-rejection trick — the
|
||||||
|
* measurement with the shortest delay is the most accurate because
|
||||||
|
* less network jitter could have skewed the timestamps.
|
||||||
|
*
|
||||||
|
* Polling: greedy mode at 1s intervals for the first 3 pings to warm
|
||||||
|
* up the offset, then low-profile at 60s intervals for steady-state.
|
||||||
|
* `forceUpdate()` resets to greedy mode (called on group join).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Api } from "@jellyfin/sdk";
|
||||||
|
import { getTimeSyncApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
|
import { EventEmitter } from "../EventEmitter";
|
||||||
|
|
||||||
|
const NumberOfTrackedMeasurements = 8;
|
||||||
|
const PollingIntervalGreedy = 1000; // ms
|
||||||
|
const PollingIntervalLowProfile = 60000; // ms
|
||||||
|
const GreedyPingCount = 3;
|
||||||
|
|
||||||
|
class Measurement {
|
||||||
|
requestSent: number;
|
||||||
|
requestReceived: number;
|
||||||
|
responseSent: number;
|
||||||
|
responseReceived: number;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
requestSent: Date,
|
||||||
|
requestReceived: Date,
|
||||||
|
responseSent: Date,
|
||||||
|
responseReceived: Date,
|
||||||
|
) {
|
||||||
|
this.requestSent = requestSent.getTime();
|
||||||
|
this.requestReceived = requestReceived.getTime();
|
||||||
|
this.responseSent = responseSent.getTime();
|
||||||
|
this.responseReceived = responseReceived.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Time offset (ms): positive means server clock is ahead of ours. */
|
||||||
|
getOffset(): number {
|
||||||
|
return (
|
||||||
|
(this.requestReceived -
|
||||||
|
this.requestSent +
|
||||||
|
(this.responseSent - this.responseReceived)) /
|
||||||
|
2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Round-trip delay (ms), excluding server processing. */
|
||||||
|
getDelay(): number {
|
||||||
|
return (
|
||||||
|
this.responseReceived -
|
||||||
|
this.requestSent -
|
||||||
|
(this.responseSent - this.requestReceived)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One-way ping (ms). */
|
||||||
|
getPing(): number {
|
||||||
|
return this.getDelay() / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks the offset between this client's clock and the Jellyfin server's
|
||||||
|
* clock, and exposes conversions between local and remote Dates.
|
||||||
|
*
|
||||||
|
* Listeners:
|
||||||
|
* - `"update"` (timeOffset: number, ping: number) — fires on every
|
||||||
|
* successful ping. Errors are logged but not emitted; consumers
|
||||||
|
* should treat absence of updates as transient.
|
||||||
|
*/
|
||||||
|
export class TimeSync extends EventEmitter {
|
||||||
|
private api: Api;
|
||||||
|
private pingStop = true;
|
||||||
|
private pollingInterval = PollingIntervalGreedy;
|
||||||
|
private poller: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
private pings = 0;
|
||||||
|
private measurement: Measurement | null = null;
|
||||||
|
private measurements: Measurement[] = [];
|
||||||
|
|
||||||
|
constructor(api: Api) {
|
||||||
|
super();
|
||||||
|
this.api = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Called when the user switches Jellyfin servers. */
|
||||||
|
updateApiClient(api: Api): void {
|
||||||
|
this.api = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether we've completed at least one successful measurement. */
|
||||||
|
isReady(): boolean {
|
||||||
|
return !!this.measurement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Current best-estimate time offset (ms). */
|
||||||
|
getTimeOffset(): number {
|
||||||
|
return this.measurement ? this.measurement.getOffset() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Current best-estimate one-way ping (ms). */
|
||||||
|
getPing(): number {
|
||||||
|
return this.measurement ? this.measurement.getPing() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convert a server-time Date to local time. */
|
||||||
|
remoteDateToLocal(remote: Date): Date {
|
||||||
|
return new Date(remote.getTime() - this.getTimeOffset());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convert a local Date to server time. */
|
||||||
|
localDateToRemote(local: Date): Date {
|
||||||
|
return new Date(local.getTime() + this.getTimeOffset());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Start polling. Idempotent. */
|
||||||
|
startPing(): void {
|
||||||
|
this.pingStop = false;
|
||||||
|
this.scheduleNextPing();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stop polling. Idempotent. */
|
||||||
|
stopPing(): void {
|
||||||
|
this.pingStop = true;
|
||||||
|
if (this.poller) {
|
||||||
|
clearTimeout(this.poller);
|
||||||
|
this.poller = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reset to greedy polling and force a fresh measurement immediately. */
|
||||||
|
forceUpdate(): void {
|
||||||
|
this.stopPing();
|
||||||
|
this.pollingInterval = PollingIntervalGreedy;
|
||||||
|
this.pings = 0;
|
||||||
|
this.startPing();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Drop all measurements. Used on group leave. */
|
||||||
|
resetMeasurements(): void {
|
||||||
|
this.measurement = null;
|
||||||
|
this.measurements = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Full teardown on provider unmount. */
|
||||||
|
destroy(): void {
|
||||||
|
this.stopPing();
|
||||||
|
this.resetMeasurements();
|
||||||
|
this.removeAllListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
private scheduleNextPing(): void {
|
||||||
|
if (this.poller || this.pingStop) return;
|
||||||
|
this.poller = setTimeout(() => {
|
||||||
|
this.poller = null;
|
||||||
|
this.requestPing()
|
||||||
|
.then((result) => this.onPingResponse(result))
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("SyncPlay TimeSync: ping failed", error);
|
||||||
|
})
|
||||||
|
.finally(() => this.scheduleNextPing());
|
||||||
|
}, this.pollingInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async requestPing() {
|
||||||
|
const requestSent = new Date();
|
||||||
|
const response = await getTimeSyncApi(this.api).getUtcTime();
|
||||||
|
const responseReceived = new Date();
|
||||||
|
const data = response.data;
|
||||||
|
const requestReceived = new Date(data.RequestReceptionTime as string);
|
||||||
|
const responseSent = new Date(data.ResponseTransmissionTime as string);
|
||||||
|
return { requestSent, requestReceived, responseSent, responseReceived };
|
||||||
|
}
|
||||||
|
|
||||||
|
private onPingResponse(result: {
|
||||||
|
requestSent: Date;
|
||||||
|
requestReceived: Date;
|
||||||
|
responseSent: Date;
|
||||||
|
responseReceived: Date;
|
||||||
|
}): void {
|
||||||
|
const measurement = new Measurement(
|
||||||
|
result.requestSent,
|
||||||
|
result.requestReceived,
|
||||||
|
result.responseSent,
|
||||||
|
result.responseReceived,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.measurements.push(measurement);
|
||||||
|
if (this.measurements.length > NumberOfTrackedMeasurements) {
|
||||||
|
this.measurements.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outlier rejection: pick the measurement with the shortest delay.
|
||||||
|
const sorted = [...this.measurements].sort(
|
||||||
|
(a, b) => a.getDelay() - b.getDelay(),
|
||||||
|
);
|
||||||
|
this.measurement = sorted[0];
|
||||||
|
|
||||||
|
// Throttle once we've warmed up.
|
||||||
|
if (this.pings >= GreedyPingCount) {
|
||||||
|
this.pollingInterval = PollingIntervalLowProfile;
|
||||||
|
} else {
|
||||||
|
this.pings++;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit("update", this.getTimeOffset(), this.getPing());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TimeSync;
|
||||||
13
providers/SyncPlay/index.ts
Normal file
13
providers/SyncPlay/index.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* SyncPlay — public exports.
|
||||||
|
*
|
||||||
|
* Only what external consumers (components, hooks, screens) need.
|
||||||
|
* Internal modules (PlaybackCore, QueueCore, TimeSync, PlayerWrapper,
|
||||||
|
* queueTranslation, EventEmitter, etc.) stay package-private.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { Controller as SyncPlayController } from "./Controller";
|
||||||
|
export { msToTicks, ticksToMs } from "./constants";
|
||||||
|
export { SyncPlayManager } from "./Manager";
|
||||||
|
export { SyncPlayProvider, useSyncPlay } from "./SyncPlayProvider";
|
||||||
|
export * from "./types";
|
||||||
58
providers/SyncPlay/player/PendingPlaybackTracker.ts
Normal file
58
providers/SyncPlay/player/PendingPlaybackTracker.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* PendingPlaybackTracker — tracks an in-flight `Unpause` / `Pause` request
|
||||||
|
* that we've sent to the server but haven't seen echoed back via
|
||||||
|
* `SyncPlayCommand`.
|
||||||
|
*
|
||||||
|
* Drives three things:
|
||||||
|
* 1. Drop duplicate rapid taps
|
||||||
|
* 2. Provide an optimistic-UI hint for the in-flight state
|
||||||
|
* 3. Override "current play state" when deciding pause-vs-unpause
|
||||||
|
* for the next tap
|
||||||
|
*
|
||||||
|
* Auto-clears after `pendingPlaybackTimeoutMs` so a lost broadcast
|
||||||
|
* doesn't freeze the UI forever.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { SYNC_PLAY_TUNING } from "../types";
|
||||||
|
|
||||||
|
export class PendingPlaybackTracker {
|
||||||
|
private command: "Unpause" | "Pause" | null = null;
|
||||||
|
private timeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
private onChange: ((cmd: "Unpause" | "Pause" | null) => void) | null = null;
|
||||||
|
|
||||||
|
setChangeHandler(
|
||||||
|
handler: ((cmd: "Unpause" | "Pause" | null) => void) | null,
|
||||||
|
): void {
|
||||||
|
this.onChange = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(): "Unpause" | "Pause" | null {
|
||||||
|
return this.command;
|
||||||
|
}
|
||||||
|
|
||||||
|
mark(command: "Unpause" | "Pause"): void {
|
||||||
|
this.command = command;
|
||||||
|
if (this.timeout) clearTimeout(this.timeout);
|
||||||
|
this.timeout = setTimeout(() => {
|
||||||
|
console.debug(
|
||||||
|
"SyncPlay PendingPlaybackTracker: timed out waiting for broadcast",
|
||||||
|
command,
|
||||||
|
);
|
||||||
|
this.command = null;
|
||||||
|
this.timeout = null;
|
||||||
|
this.onChange?.(null);
|
||||||
|
}, SYNC_PLAY_TUNING.pendingPlaybackTimeoutMs);
|
||||||
|
this.onChange?.(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
if (this.timeout) {
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
this.timeout = null;
|
||||||
|
}
|
||||||
|
if (this.command !== null) {
|
||||||
|
this.command = null;
|
||||||
|
this.onChange?.(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
87
providers/SyncPlay/player/PlayerWrapper.ts
Normal file
87
providers/SyncPlay/player/PlayerWrapper.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* PlayerWrapper — adapter between jellyfin's tick-based playerWrapper API
|
||||||
|
* and our millisecond-based `PlayerControls`. Methods that have no RN
|
||||||
|
* analog (queue mutation hooks) delegate to provider-supplied handlers
|
||||||
|
* which navigate to the player screen.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { TicksPerMillisecond } from "../constants";
|
||||||
|
import type { PlayerControls } from "../types";
|
||||||
|
|
||||||
|
/** Options passed to `playerWrapper.localPlay` — provider navigates to the player screen. */
|
||||||
|
export interface LocalPlayOptions {
|
||||||
|
ids: (string | undefined)[];
|
||||||
|
startPositionTicks: number;
|
||||||
|
startIndex: number;
|
||||||
|
serverId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PlayerWrapper {
|
||||||
|
private controls: PlayerControls | null = null;
|
||||||
|
private localPlayHandler: ((options: LocalPlayOptions) => void) | null = null;
|
||||||
|
private setCurrentItemHandler:
|
||||||
|
| ((playlistItemId: string | null) => void)
|
||||||
|
| null = null;
|
||||||
|
|
||||||
|
/** Attach / detach the underlying player. */
|
||||||
|
bindToControls(controls: PlayerControls | null): void {
|
||||||
|
this.controls = controls;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Provider wires this to navigate to the player screen. */
|
||||||
|
setLocalPlayHandler(handler: ((options: LocalPlayOptions) => void) | null) {
|
||||||
|
this.localPlayHandler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Provider wires this to navigate to a different queue item. */
|
||||||
|
setLocalSetCurrentItemHandler(
|
||||||
|
handler: ((playlistItemId: string | null) => void) | null,
|
||||||
|
) {
|
||||||
|
this.setCurrentItemHandler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
localUnpause(): void {
|
||||||
|
this.controls?.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
localPause(): void {
|
||||||
|
this.controls?.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Upstream takes ticks; RN's `seekTo` takes ms. */
|
||||||
|
localSeek(positionTicks: number): void {
|
||||||
|
this.controls?.seekTo(positionTicks / TicksPerMillisecond);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** RN: pause instead of teardown — leaving the player screen is the navigator's job. */
|
||||||
|
localStop(): void {
|
||||||
|
this.controls?.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Position in ms. */
|
||||||
|
currentTime(): number {
|
||||||
|
return this.controls?.getCurrentPosition() ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
isPlaying(): boolean {
|
||||||
|
return this.controls?.isPlaying() ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
isPlaybackActive(): boolean {
|
||||||
|
return this.controls !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** RN never runs as a remote-managed player. */
|
||||||
|
isRemote(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
localPlay(options: LocalPlayOptions): Promise<void> {
|
||||||
|
this.localPlayHandler?.(options);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
localSetCurrentPlaylistItem(playlistItemId: string | null): void {
|
||||||
|
this.setCurrentItemHandler?.(playlistItemId);
|
||||||
|
}
|
||||||
|
}
|
||||||
64
providers/SyncPlay/player/bufferingDebouncer.ts
Normal file
64
providers/SyncPlay/player/bufferingDebouncer.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* bufferingDebouncer — wrap an `isBuffering: boolean → void` notify callback
|
||||||
|
* with three RN-only guards. Web gets these for free from HTML `waiting`/
|
||||||
|
* `canplay`; our `PlayerControls` exposes state (not events) and the React
|
||||||
|
* effect that polls it can fire many times per second.
|
||||||
|
*
|
||||||
|
* - **dedup**: drop redundant calls when state hasn't changed
|
||||||
|
* - **debounce buffering→true**: only escalate after the threshold;
|
||||||
|
* going back to ready cancels the pending escalation
|
||||||
|
* - **coalesce inflight**: serialize concurrent sends
|
||||||
|
*
|
||||||
|
* Returns `{ notify, dispose }`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { SYNC_PLAY_TUNING } from "../types";
|
||||||
|
|
||||||
|
export function createBufferingDebouncer(
|
||||||
|
send: (isBuffering: boolean) => Promise<void>,
|
||||||
|
) {
|
||||||
|
let lastSent: boolean | null = null;
|
||||||
|
let inflight: Promise<void> | null = null;
|
||||||
|
let pendingTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
const flush = async (isBuffering: boolean) => {
|
||||||
|
if (lastSent === isBuffering) return;
|
||||||
|
if (inflight) {
|
||||||
|
try {
|
||||||
|
await inflight;
|
||||||
|
} catch {
|
||||||
|
// ignore — used only for ordering
|
||||||
|
}
|
||||||
|
if (lastSent === isBuffering) return;
|
||||||
|
}
|
||||||
|
lastSent = isBuffering;
|
||||||
|
inflight = send(isBuffering).finally(() => {
|
||||||
|
inflight = null;
|
||||||
|
});
|
||||||
|
return inflight;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
notify(isBuffering: boolean): void {
|
||||||
|
if (pendingTimeout) {
|
||||||
|
clearTimeout(pendingTimeout);
|
||||||
|
pendingTimeout = null;
|
||||||
|
}
|
||||||
|
if (!isBuffering) {
|
||||||
|
// Ready always fires immediately.
|
||||||
|
void flush(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingTimeout = setTimeout(() => {
|
||||||
|
pendingTimeout = null;
|
||||||
|
void flush(true);
|
||||||
|
}, SYNC_PLAY_TUNING.minBufferingThresholdMs);
|
||||||
|
},
|
||||||
|
dispose(): void {
|
||||||
|
if (pendingTimeout) {
|
||||||
|
clearTimeout(pendingTimeout);
|
||||||
|
pendingTimeout = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
58
providers/SyncPlay/player/reconcileToGroupOnAttach.ts
Normal file
58
providers/SyncPlay/player/reconcileToGroupOnAttach.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* reconcileToGroupOnAttach — estimate the group's current position from
|
||||||
|
* the last play/pause broadcast and seek the freshly-attached player
|
||||||
|
* there if drift exceeds the threshold.
|
||||||
|
*
|
||||||
|
* Web's player binds at group-join, so this race doesn't exist there.
|
||||||
|
* On RN the player mounts in a separate route after the join, so
|
||||||
|
* commands arrive before controls attach. Without this, the player
|
||||||
|
* resumes from its local position and is silently behind the group.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { TicksPerMillisecond } from "../constants";
|
||||||
|
import {
|
||||||
|
type PlayerControls,
|
||||||
|
type SendCommand,
|
||||||
|
SYNC_PLAY_TUNING,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
|
export function reconcileToGroupOnAttach(
|
||||||
|
controls: PlayerControls,
|
||||||
|
lastCommand: SendCommand | null,
|
||||||
|
localToRemote: (local: Date) => Date,
|
||||||
|
): void {
|
||||||
|
if (
|
||||||
|
!lastCommand ||
|
||||||
|
(lastCommand.Command !== "Unpause" && lastCommand.Command !== "Pause") ||
|
||||||
|
!lastCommand.When ||
|
||||||
|
lastCommand.PositionTicks == null
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const commandWhen = new Date(lastCommand.When);
|
||||||
|
let targetTicks = lastCommand.PositionTicks;
|
||||||
|
if (lastCommand.Command === "Unpause") {
|
||||||
|
const remoteNow = localToRemote(new Date());
|
||||||
|
targetTicks +=
|
||||||
|
(remoteNow.getTime() - commandWhen.getTime()) * TicksPerMillisecond;
|
||||||
|
}
|
||||||
|
const targetMs = Math.max(0, targetTicks / TicksPerMillisecond);
|
||||||
|
const currentMs = controls.getCurrentPosition();
|
||||||
|
if (
|
||||||
|
Math.abs(currentMs - targetMs) >
|
||||||
|
SYNC_PLAY_TUNING.positionReconcileThresholdMs
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
`SyncPlay: player attached — seeking to estimated group position ${targetMs}ms (was ${currentMs}ms)`,
|
||||||
|
);
|
||||||
|
controls.seekTo(targetMs);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
"SyncPlay: failed to estimate group position on attach",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
183
providers/SyncPlay/transport/queueTranslation.ts
Normal file
183
providers/SyncPlay/transport/queueTranslation.ts
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
/**
|
||||||
|
* queueTranslation — expand container items into a real playable queue.
|
||||||
|
*
|
||||||
|
* The server takes the queue we send via `syncPlaySetNewQueue` and
|
||||||
|
* rebroadcasts it verbatim to every group member. Sending a container
|
||||||
|
* ID (Series, Season, BoxSet, Playlist) means every receiver fails to
|
||||||
|
* open the player because they can't directly play a container. We must
|
||||||
|
* expand to real playable item IDs before sending the queue.
|
||||||
|
*
|
||||||
|
* Video-focused: music (MusicArtist/MusicGenre) and photo branches are
|
||||||
|
* intentionally omitted. Live TV (Program), Episode auto-advance, and
|
||||||
|
* folder expansion are preserved because they're the common video flows.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Api } from "@jellyfin/sdk";
|
||||||
|
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||||
|
import {
|
||||||
|
getItemsApi,
|
||||||
|
getTvShowsApi,
|
||||||
|
getUserApi,
|
||||||
|
getUserLibraryApi,
|
||||||
|
} from "@jellyfin/sdk/lib/utils/api";
|
||||||
|
|
||||||
|
export interface TranslateOptions {
|
||||||
|
ids?: string[];
|
||||||
|
shuffle?: boolean;
|
||||||
|
queryOptions?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PLAYBACK_FIELDS = ["Chapters", "Trickplay"] as const;
|
||||||
|
|
||||||
|
async function getCurrentUser(api: Api) {
|
||||||
|
const user = (await getUserApi(api).getCurrentUser()).data;
|
||||||
|
if (!user?.Id) {
|
||||||
|
throw new Error("SyncPlay queueTranslation: no authenticated user");
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function queryItems(
|
||||||
|
api: Api,
|
||||||
|
userId: string,
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
): Promise<BaseItemDto[]> {
|
||||||
|
const res = await getItemsApi(api).getItems({
|
||||||
|
limit: 300,
|
||||||
|
fields: PLAYBACK_FIELDS as unknown as never,
|
||||||
|
excludeLocationTypes: ["Virtual"] as unknown as never,
|
||||||
|
enableTotalRecordCount: false,
|
||||||
|
collapseBoxSetItems: false,
|
||||||
|
...params,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
return res.data.Items ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchFolderChildren(
|
||||||
|
api: Api,
|
||||||
|
userId: string,
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
): Promise<BaseItemDto[]> {
|
||||||
|
return queryItems(api, userId, {
|
||||||
|
filters: ["IsNotFolder"],
|
||||||
|
recursive: true,
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve item IDs into full `BaseItemDto`s.
|
||||||
|
*
|
||||||
|
* - single ID → `getItem` (cheap, no Items wrapper)
|
||||||
|
* - multi ID → `getItems` with playback defaults
|
||||||
|
*/
|
||||||
|
export async function getItemsForPlayback(
|
||||||
|
api: Api,
|
||||||
|
ids: string[],
|
||||||
|
): Promise<BaseItemDto[]> {
|
||||||
|
if (!ids.length) return [];
|
||||||
|
const userId = (await getCurrentUser(api)).Id as string;
|
||||||
|
if (ids.length === 1) {
|
||||||
|
const res = await getUserLibraryApi(api).getItem({
|
||||||
|
userId,
|
||||||
|
itemId: ids[0],
|
||||||
|
});
|
||||||
|
return res.data ? [res.data] : [];
|
||||||
|
}
|
||||||
|
return queryItems(api, userId, { ids });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expand a "first item" into a real playable queue.
|
||||||
|
*
|
||||||
|
* - Program → channel items
|
||||||
|
* - Playlist → playlist children
|
||||||
|
* - IsFolder (Series, Season, BoxSet, MusicAlbum, ...) → recursive descendants
|
||||||
|
* - single Episode (when `EnableNextEpisodeAutoPlay`) → remaining series episodes
|
||||||
|
* - everything else → passthrough (Movies, Audio, single Episode w/ autoplay off)
|
||||||
|
*
|
||||||
|
* Preserves the caller's `ids` order so the receiver sees the same
|
||||||
|
* queue order the sender intended.
|
||||||
|
*/
|
||||||
|
export async function translateItemsForPlayback(
|
||||||
|
api: Api,
|
||||||
|
items: BaseItemDto[],
|
||||||
|
options: TranslateOptions = {},
|
||||||
|
): Promise<BaseItemDto[]> {
|
||||||
|
if (!items.length) return [];
|
||||||
|
|
||||||
|
const workingItems =
|
||||||
|
items.length > 1 && options.ids
|
||||||
|
? [...items].sort(
|
||||||
|
(a, b) =>
|
||||||
|
(options.ids ?? []).indexOf(a.Id ?? "") -
|
||||||
|
(options.ids ?? []).indexOf(b.Id ?? ""),
|
||||||
|
)
|
||||||
|
: items;
|
||||||
|
|
||||||
|
const firstItem = workingItems[0];
|
||||||
|
|
||||||
|
if (firstItem.Type === "Program" && firstItem.ChannelId) {
|
||||||
|
return getItemsForPlayback(api, [firstItem.ChannelId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await getCurrentUser(api);
|
||||||
|
const userId = user.Id as string;
|
||||||
|
|
||||||
|
if (firstItem.Type === "Playlist") {
|
||||||
|
return queryItems(api, userId, {
|
||||||
|
parentId: firstItem.Id,
|
||||||
|
sortBy: options.shuffle ? ["Random"] : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstItem.IsFolder) {
|
||||||
|
// Series, Season, BoxSet, MusicAlbum, etc.
|
||||||
|
const sortBy = options.shuffle
|
||||||
|
? ["Random"]
|
||||||
|
: firstItem.Type === "BoxSet"
|
||||||
|
? ["SortName"]
|
||||||
|
: undefined;
|
||||||
|
return fetchFolderChildren(api, userId, {
|
||||||
|
parentId: firstItem.Id,
|
||||||
|
mediaTypes: ["Audio", "Video"],
|
||||||
|
sortBy,
|
||||||
|
...(options.queryOptions ?? {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstItem.Type === "Episode" && workingItems.length === 1) {
|
||||||
|
// Single-episode auto-next: load all remaining episodes in the
|
||||||
|
// series, starting at this one. Gated on the user preference so we
|
||||||
|
// don't surprise users who disabled autoplay.
|
||||||
|
if (!user.Configuration?.EnableNextEpisodeAutoPlay || !firstItem.SeriesId) {
|
||||||
|
return workingItems;
|
||||||
|
}
|
||||||
|
const res = await getTvShowsApi(api).getEpisodes({
|
||||||
|
seriesId: firstItem.SeriesId,
|
||||||
|
userId,
|
||||||
|
isMissing: false,
|
||||||
|
fields: PLAYBACK_FIELDS as unknown as never,
|
||||||
|
// SDK omits `isVirtualUnaired` from typed request; server honours
|
||||||
|
// it. Cast keeps wire payload identical to jellyfin-web.
|
||||||
|
...({ isVirtualUnaired: false } as Record<string, unknown>),
|
||||||
|
} as Parameters<ReturnType<typeof getTvShowsApi>["getEpisodes"]>[0]);
|
||||||
|
const all = res.data.Items ?? [];
|
||||||
|
// Drop everything before firstItem; keep firstItem and everything
|
||||||
|
// after. Empty list if firstItem isn't in the series (shouldn't
|
||||||
|
// happen, but matches upstream's behaviour).
|
||||||
|
let foundItem = false;
|
||||||
|
return all.filter((e) => {
|
||||||
|
if (foundItem) return true;
|
||||||
|
if (e.Id === firstItem.Id) {
|
||||||
|
foundItem = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Movies, Audio, single Episode w/ autoplay off, etc.
|
||||||
|
return workingItems;
|
||||||
|
}
|
||||||
94
providers/SyncPlay/transport/useSyncPlayWebSocket.ts
Normal file
94
providers/SyncPlay/transport/useSyncPlayWebSocket.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* useSyncPlayWebSocket
|
||||||
|
*
|
||||||
|
* Hook that connects the SyncPlay manager to WebSocket messages.
|
||||||
|
* Listens for SyncPlayCommand and SyncPlayGroupUpdate messages.
|
||||||
|
*
|
||||||
|
* IMPORTANT: We subscribe directly to the WebSocket via `addEventListener`
|
||||||
|
* rather than reading WebSocketProvider's `lastMessage` state. That state
|
||||||
|
* only holds the most recent message, so when the server emits bursts
|
||||||
|
* after a join (GroupJoined + StateUpdate + UserJoined + PlayQueue, all
|
||||||
|
* within a few ms), React's batching causes earlier messages to be
|
||||||
|
* overwritten before our effect can read them — most notably the
|
||||||
|
* GroupJoined message, which left the joining client thinking it hadn't
|
||||||
|
* joined while other members already saw it as a participant.
|
||||||
|
*
|
||||||
|
* Listening on the raw socket guarantees we see every frame in order.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useWebSocketContext } from "@/providers/WebSocketProvider";
|
||||||
|
import type { SyncPlayManager } from "../Manager";
|
||||||
|
import type { GroupUpdate, SendCommand } from "../types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to connect SyncPlay manager to WebSocket
|
||||||
|
*/
|
||||||
|
export function useSyncPlayWebSocket(manager: SyncPlayManager | null): void {
|
||||||
|
const { ws } = useWebSocketContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ws || !manager) return;
|
||||||
|
|
||||||
|
const handleMessage = (event: WebSocketMessageEvent) => {
|
||||||
|
let parsed: { MessageType?: string; Data?: unknown };
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(event.data as string);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("SyncPlay: failed to parse WebSocket message", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { MessageType, Data } = parsed;
|
||||||
|
|
||||||
|
// Only handle SyncPlay messages here; everything else is handled
|
||||||
|
// elsewhere via WebSocketProvider's lastMessage.
|
||||||
|
if (!MessageType?.startsWith("SyncPlay")) return;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`SyncPlay WebSocket [${MessageType}]:`,
|
||||||
|
JSON.stringify(Data).substring(0, 300),
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (MessageType) {
|
||||||
|
case "SyncPlayCommand": {
|
||||||
|
const command = Data as SendCommand;
|
||||||
|
console.log(
|
||||||
|
`SyncPlay: COMMAND received - ${command.Command} at ${command.When}`,
|
||||||
|
command.Command === "Seek"
|
||||||
|
? `position=${command.PositionTicks}`
|
||||||
|
: "",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Note: it's normal for controls to be missing here during the
|
||||||
|
// join → navigate → load window. Manager stashes the command and
|
||||||
|
// replays it on attach.
|
||||||
|
manager.processCommand(command);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "SyncPlayGroupUpdate": {
|
||||||
|
// SDK's `GroupUpdate` type is a discriminated union with a
|
||||||
|
// narrower `Type` enum than the wire format. Cast through
|
||||||
|
// unknown so upstream `Manager.processGroupUpdate` can switch
|
||||||
|
// on the real string.
|
||||||
|
const update = Data as unknown as GroupUpdate;
|
||||||
|
console.debug(
|
||||||
|
"SyncPlay: group update -",
|
||||||
|
(update as { Type?: string }).Type,
|
||||||
|
);
|
||||||
|
manager.processGroupUpdate(update);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.addEventListener("message", handleMessage);
|
||||||
|
return () => {
|
||||||
|
ws.removeEventListener("message", handleMessage);
|
||||||
|
};
|
||||||
|
}, [ws, manager]);
|
||||||
|
}
|
||||||
88
providers/SyncPlay/types.ts
Normal file
88
providers/SyncPlay/types.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* SyncPlay — public types and tuning constants.
|
||||||
|
*
|
||||||
|
* Re-exports the SDK types we use, defines the small RN-specific
|
||||||
|
* extensions (PlayerControls, OSD actions), and centralises the magic
|
||||||
|
* numbers that govern sync behaviour.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||||
|
|
||||||
|
// SDK type re-exports — kept narrow on purpose, only what callers
|
||||||
|
// actually reach for.
|
||||||
|
export type {
|
||||||
|
GroupInfoDto,
|
||||||
|
GroupQueueMode,
|
||||||
|
GroupRepeatMode,
|
||||||
|
GroupShuffleMode,
|
||||||
|
GroupStateType,
|
||||||
|
GroupUpdate,
|
||||||
|
PlayQueueUpdate,
|
||||||
|
PlayQueueUpdateReason,
|
||||||
|
SendCommand,
|
||||||
|
SendCommandType,
|
||||||
|
SyncPlayQueueItem,
|
||||||
|
SyncPlayUserAccessType,
|
||||||
|
} from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
|
|
||||||
|
/** Jellyfin's tick unit. 1ms = 10000 ticks. */
|
||||||
|
export const TicksPerMillisecond = 10000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Player controls SyncPlay drives. The provider wires this up against
|
||||||
|
* the active RN player (mpv / VLC / expo-video).
|
||||||
|
*/
|
||||||
|
export interface PlayerControls {
|
||||||
|
play: () => void;
|
||||||
|
pause: () => void;
|
||||||
|
/** Seek to absolute position in milliseconds. */
|
||||||
|
seekTo: (positionMs: number) => void;
|
||||||
|
setSpeed: (speed: number) => void;
|
||||||
|
getSpeed: () => number;
|
||||||
|
/** Current position in milliseconds. */
|
||||||
|
getCurrentPosition: () => number;
|
||||||
|
isPlaying: () => boolean;
|
||||||
|
isBuffering: () => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** OSD action types — drive optional player-overlay feedback. */
|
||||||
|
export type SyncPlayOsdAction =
|
||||||
|
/** transient — 1.5s pulse, the unpause command fired locally */
|
||||||
|
| "unpause"
|
||||||
|
/** transient — 1.5s pulse, the pause command fired locally */
|
||||||
|
| "pause"
|
||||||
|
/** transient — 1.5s pulse, a seek command applied locally */
|
||||||
|
| "seek"
|
||||||
|
/** persistent — group is about to play (Waiting+Unpause / pending Unpause) */
|
||||||
|
| "schedule-play"
|
||||||
|
/** persistent — another client is buffering (Waiting+Buffer) */
|
||||||
|
| "buffering"
|
||||||
|
/** persistent — group transitioning to pause (Waiting+Pause) */
|
||||||
|
| "wait-pause"
|
||||||
|
/** persistent — group transitioning to unpause; sibling of schedule-play */
|
||||||
|
| "wait-unpause";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tuning constants. These mirror jellyfin-web's defaults; tweak with
|
||||||
|
* care — they affect perceived sync quality across all clients.
|
||||||
|
*/
|
||||||
|
export const SYNC_PLAY_TUNING = {
|
||||||
|
/** Drift threshold (ms) above which we hard-seek to catch up. */
|
||||||
|
minDelaySkipToSync: 400,
|
||||||
|
/** Drift beyond this (ms) is always corrected by seeking. */
|
||||||
|
maxDelaySync: 3000,
|
||||||
|
/** Don't escalate buffering to the group for blips shorter than this (ms). */
|
||||||
|
minBufferingThresholdMs: 3000,
|
||||||
|
/** Player-attach drift (ms) above which we reconcile to group position. */
|
||||||
|
positionReconcileThresholdMs: 500,
|
||||||
|
/** Safety timeout (ms) for in-flight Pause/Unpause optimistic UI. */
|
||||||
|
pendingPlaybackTimeoutMs: 1500,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** Options accepted by `Controller.play`. */
|
||||||
|
export interface PlayOptions {
|
||||||
|
ids?: string[];
|
||||||
|
items?: BaseItemDto[];
|
||||||
|
startIndex?: number;
|
||||||
|
startPositionTicks?: number;
|
||||||
|
}
|
||||||
@@ -44,6 +44,15 @@ interface WebSocketContextType {
|
|||||||
lastMessage: WebSocketMessage | null;
|
lastMessage: WebSocketMessage | null;
|
||||||
sendMessage: (message: any) => void;
|
sendMessage: (message: any) => void;
|
||||||
clearLastMessage: () => void;
|
clearLastMessage: () => void;
|
||||||
|
/**
|
||||||
|
* Acquire a keep-alive token. While at least one token is held the
|
||||||
|
* WebSocket will NOT be closed on AppState background/inactive. Used
|
||||||
|
* by the video player while in Picture-in-Picture so SyncPlay (and
|
||||||
|
* any other server-pushed events) keep flowing. Returns a release
|
||||||
|
* function — call it (or rely on the React effect cleanup) when the
|
||||||
|
* keep-alive is no longer needed.
|
||||||
|
*/
|
||||||
|
acquireKeepAlive: () => () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const WebSocketContext = createContext<WebSocketContextType | null>(null);
|
const WebSocketContext = createContext<WebSocketContextType | null>(null);
|
||||||
@@ -63,6 +72,21 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => {
|
|||||||
const libraryChangeDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(
|
const libraryChangeDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
// Ref-counted keep-alive: while > 0 we skip the AppState→background
|
||||||
|
// close so the socket survives PiP / brief OS suspensions. iOS keeps
|
||||||
|
// the audio session (and therefore networking) alive while PiP is
|
||||||
|
// active, so the WS can continue to receive SyncPlay commands.
|
||||||
|
const keepAliveCountRef = useRef(0);
|
||||||
|
|
||||||
|
const acquireKeepAlive = useCallback((): (() => void) => {
|
||||||
|
keepAliveCountRef.current += 1;
|
||||||
|
let released = false;
|
||||||
|
return () => {
|
||||||
|
if (released) return;
|
||||||
|
released = true;
|
||||||
|
keepAliveCountRef.current = Math.max(0, keepAliveCountRef.current - 1);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const connectWebSocket = useCallback(() => {
|
const connectWebSocket = useCallback(() => {
|
||||||
if (!deviceId || !api?.accessToken || !isNetworkConnected) {
|
if (!deviceId || !api?.accessToken || !isNetworkConnected) {
|
||||||
@@ -235,9 +259,20 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleAppStateChange = (state: AppStateStatus) => {
|
const handleAppStateChange = (state: AppStateStatus) => {
|
||||||
if (state === "background" || state === "inactive") {
|
if (state === "background" || state === "inactive") {
|
||||||
|
if (keepAliveCountRef.current > 0) {
|
||||||
|
console.log(
|
||||||
|
`App backgrounded but WS keep-alive held (${keepAliveCountRef.current}); leaving socket open`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.log("App moving to background, closing WebSocket...");
|
console.log("App moving to background, closing WebSocket...");
|
||||||
ws?.close();
|
ws?.close();
|
||||||
} else if (state === "active") {
|
} else if (state === "active") {
|
||||||
|
// Only reconnect if we actually lost the socket (we may have
|
||||||
|
// skipped the close above because of a keep-alive token).
|
||||||
|
if (ws?.readyState === WebSocket.OPEN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.log("App coming to foreground, reconnecting WebSocket...");
|
console.log("App coming to foreground, reconnecting WebSocket...");
|
||||||
connectWebSocket();
|
connectWebSocket();
|
||||||
}
|
}
|
||||||
@@ -267,7 +302,14 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => {
|
|||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<WebSocketContext.Provider
|
<WebSocketContext.Provider
|
||||||
value={{ ws, isConnected, lastMessage, sendMessage, clearLastMessage }}
|
value={{
|
||||||
|
ws,
|
||||||
|
isConnected,
|
||||||
|
lastMessage,
|
||||||
|
sendMessage,
|
||||||
|
clearLastMessage,
|
||||||
|
acquireKeepAlive,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</WebSocketContext.Provider>
|
</WebSocketContext.Provider>
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
"error_title": "خطأ",
|
"error_title": "خطأ",
|
||||||
"login_title": "تسجيل الدخول",
|
"login_title": "تسجيل الدخول",
|
||||||
"login_to_title": "تسجيل الدخول إلى",
|
"login_to_title": "تسجيل الدخول إلى",
|
||||||
|
"select_user": "Select a user to log in",
|
||||||
|
"add_user_to_login": "Add a user to log in",
|
||||||
|
"add_user": "Add User",
|
||||||
"username_placeholder": "اسم المستخدم",
|
"username_placeholder": "اسم المستخدم",
|
||||||
"password_placeholder": "كلمة المرور",
|
"password_placeholder": "كلمة المرور",
|
||||||
"login_button": "تسجيل الدخول",
|
"login_button": "تسجيل الدخول",
|
||||||
@@ -30,48 +33,54 @@
|
|||||||
"connect_button": "اتصل",
|
"connect_button": "اتصل",
|
||||||
"previous_servers": "الخوادم السابقة",
|
"previous_servers": "الخوادم السابقة",
|
||||||
"clear_button": "مسح",
|
"clear_button": "مسح",
|
||||||
"swipe_to_remove": "Swipe to remove",
|
"swipe_to_remove": "مرر للإزالة",
|
||||||
"search_for_local_servers": "البحث عن الخوادم المحلية",
|
"search_for_local_servers": "البحث عن الخوادم المحلية",
|
||||||
"searching": "جاري البحث...",
|
"searching": "جاري البحث...",
|
||||||
"servers": "الخوادم",
|
"servers": "الخوادم",
|
||||||
"saved": "Saved",
|
"saved": "تم الحفظ",
|
||||||
"session_expired": "Session Expired",
|
"session_expired": "انتهت الجلسة",
|
||||||
"please_login_again": "Your saved session has expired. Please log in again.",
|
"please_login_again": "انتهت مدة صلاحية جلستك. الرجاء تسجيل الدخول مرة أخرى.",
|
||||||
"remove_saved_login": "Remove Saved Login",
|
"remove_saved_login": "إزالة تسجيل دخول محفوظ",
|
||||||
"remove_saved_login_description": "This will remove your saved credentials for this server. You'll need to enter your username and password again next time.",
|
"remove_saved_login_description": "سيؤدي هذا إلى إزالة بيانات تسجيل الدخول الخاص بك المحفوظة لهذا الخادم. ستحتاج إلى إدخال اسم المستخدم وكلمة المرور مرة أخرى في المرة القادمة.",
|
||||||
"accounts_count": "{{count}} accounts",
|
"accounts_count": "الحسابات {{count}}",
|
||||||
"select_account": "Select Account",
|
"select_account": "اختر الحساب",
|
||||||
"add_account": "Add Account",
|
"add_account": "إضافة حساب",
|
||||||
"remove_account_description": "This will remove the saved credentials for {{username}}."
|
"remove_account_description": "سيؤدي هذا إلى إزالة بيانات تسجيل الدخول لـ {{username}}.",
|
||||||
|
"remove_server": "Remove Server",
|
||||||
|
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
|
||||||
|
"select_your_server": "Select Your Server",
|
||||||
|
"add_server_to_get_started": "Add a server to get started",
|
||||||
|
"add_server": "Add Server",
|
||||||
|
"change_server": "Change Server"
|
||||||
},
|
},
|
||||||
"save_account": {
|
"save_account": {
|
||||||
"title": "Save Account",
|
"title": "حفظ الحساب",
|
||||||
"save_for_later": "Save this account",
|
"save_for_later": "حفظ هذا الحساب",
|
||||||
"security_option": "Security Option",
|
"security_option": "خيارات الأمان",
|
||||||
"no_protection": "No protection",
|
"no_protection": "بدون حماية",
|
||||||
"no_protection_desc": "Quick login without authentication",
|
"no_protection_desc": "تسجيل دخول سريع بدون مصادقة",
|
||||||
"pin_code": "PIN code",
|
"pin_code": "رمز PIN",
|
||||||
"pin_code_desc": "4-digit PIN required when switching",
|
"pin_code_desc": "رمز PIN مكون من 4 أرقام مطلوب عند التبديل",
|
||||||
"password": "Re-enter password",
|
"password": "أعد إدخال كلمة المرور",
|
||||||
"password_desc": "Password required when switching",
|
"password_desc": "كلمة المرور مطلوبة عند التبديل",
|
||||||
"save_button": "Save",
|
"save_button": "حفظ",
|
||||||
"cancel_button": "Cancel"
|
"cancel_button": "إلغاء"
|
||||||
},
|
},
|
||||||
"pin": {
|
"pin": {
|
||||||
"enter_pin": "Enter PIN",
|
"enter_pin": "أدخل رمز PIN",
|
||||||
"enter_pin_for": "Enter PIN for {{username}}",
|
"enter_pin_for": "أدخل رمز PIN لـ {{username}}",
|
||||||
"enter_4_digits": "Enter 4 digits",
|
"enter_4_digits": "ادخل 4 أرقام",
|
||||||
"invalid_pin": "Invalid PIN",
|
"invalid_pin": "PIN غير صالح",
|
||||||
"setup_pin": "Set Up PIN",
|
"setup_pin": "تعيين رمز PIN",
|
||||||
"confirm_pin": "Confirm PIN",
|
"confirm_pin": "تأكيد رمز PIN",
|
||||||
"pins_dont_match": "PINs don't match",
|
"pins_dont_match": "رموز PIN غير متطابقة",
|
||||||
"forgot_pin": "Forgot PIN?",
|
"forgot_pin": "نسيت رمز PIN؟",
|
||||||
"forgot_pin_desc": "Your saved credentials will be removed"
|
"forgot_pin_desc": "سيتم إزالة بيانات تسجيل الدخول المحفوظة الخاصة بك"
|
||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"enter_password": "Enter Password",
|
"enter_password": "أدخل كلمة المرور",
|
||||||
"enter_password_for": "Enter password for {{username}}",
|
"enter_password_for": "أدخل كلمة المرور لـ {{username}}",
|
||||||
"invalid_password": "Invalid password"
|
"invalid_password": "كلمة المرور غير صحيحة"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"checking_server_connection": "التحقق من اتصال الخادم...",
|
"checking_server_connection": "التحقق من اتصال الخادم...",
|
||||||
@@ -86,8 +95,9 @@
|
|||||||
"oops": "عفوًا!",
|
"oops": "عفوًا!",
|
||||||
"error_message": "حدث خطأ ما.\nيرجى تسجيل الخروج ثم الدخول مرة أخرى.",
|
"error_message": "حدث خطأ ما.\nيرجى تسجيل الخروج ثم الدخول مرة أخرى.",
|
||||||
"continue_watching": "متابعة المشاهدة",
|
"continue_watching": "متابعة المشاهدة",
|
||||||
|
"continue": "Continue",
|
||||||
"next_up": "التالي",
|
"next_up": "التالي",
|
||||||
"continue_and_next_up": "Continue & Next Up",
|
"continue_and_next_up": "تابع و التالي",
|
||||||
"recently_added_in": "أضيف مؤخراً في {{libraryName}}",
|
"recently_added_in": "أضيف مؤخراً في {{libraryName}}",
|
||||||
"suggested_movies": "أفلام مقترحة",
|
"suggested_movies": "أفلام مقترحة",
|
||||||
"suggested_episodes": "حلقات مقترحة",
|
"suggested_episodes": "حلقات مقترحة",
|
||||||
@@ -109,6 +119,12 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"settings_title": "الإعدادات",
|
"settings_title": "الإعدادات",
|
||||||
"log_out_button": "تسجيل الخروج",
|
"log_out_button": "تسجيل الخروج",
|
||||||
|
"switch_user": {
|
||||||
|
"title": "Switch User",
|
||||||
|
"account": "Account",
|
||||||
|
"switch_user": "Switch User on This Server",
|
||||||
|
"current": "current"
|
||||||
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "الأقسام"
|
"title": "الأقسام"
|
||||||
},
|
},
|
||||||
@@ -120,36 +136,45 @@
|
|||||||
},
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "المظهر",
|
"title": "المظهر",
|
||||||
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
"merge_next_up_continue_watching": "دمج تابع المشاهدة والتالي",
|
||||||
"hide_remote_session_button": "Hide Remote Session Button"
|
"hide_remote_session_button": "إخفاء زر جلسة البث عن بُعد",
|
||||||
|
"show_home_backdrop": "Dynamic Home Backdrop",
|
||||||
|
"show_hero_carousel": "Hero Carousel",
|
||||||
|
"show_series_poster_on_episode": "Show Series Poster on Episodes",
|
||||||
|
"theme_music": "Theme Music",
|
||||||
|
"display_size": "Display Size",
|
||||||
|
"display_size_small": "Small",
|
||||||
|
"display_size_default": "Default",
|
||||||
|
"display_size_large": "Large",
|
||||||
|
"display_size_extra_large": "Extra Large"
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"title": "Network",
|
"title": "الشبكة",
|
||||||
"local_network": "Local Network",
|
"local_network": "الشبكة المحلية",
|
||||||
"auto_switch_enabled": "Auto-switch when at home",
|
"auto_switch_enabled": "التبديل التلقائي عند المنزل",
|
||||||
"auto_switch_description": "Automatically switch to local URL when connected to home WiFi",
|
"auto_switch_description": "التبديل تلقائياً إلى رابط URL محلي عند الاتصال بشبكة WiFi المنزلية",
|
||||||
"local_url": "Local URL",
|
"local_url": "رابط محلي",
|
||||||
"local_url_hint": "Enter your local server address (e.g., http://192.168.1.100:8096)",
|
"local_url_hint": "أدخل عنوان الخادم المحلي الخاص بك (على سبيل المثال http://192.168.1.100:8096)",
|
||||||
"local_url_placeholder": "http://192.168.1.100:8096",
|
"local_url_placeholder": "http://192.168.1.100:8096",
|
||||||
"home_wifi_networks": "Home WiFi Networks",
|
"home_wifi_networks": "شبكات WiFi المنزل",
|
||||||
"add_current_network": "Add \"{{ssid}}\"",
|
"add_current_network": "إضافة \"{{ssid}}\"",
|
||||||
"not_connected_to_wifi": "Not connected to WiFi",
|
"not_connected_to_wifi": "غير متصل بشبكة WiFi",
|
||||||
"no_networks_configured": "No networks configured",
|
"no_networks_configured": "لا توجد شبكات مكونة",
|
||||||
"add_network_hint": "Add your home WiFi network to enable auto-switching",
|
"add_network_hint": "إضافة شبكة WiFi المنزلية الخاصة بك لتمكين التبديل التلقائي",
|
||||||
"current_wifi": "Current WiFi",
|
"current_wifi": "شبكة WiFi الحالية",
|
||||||
"using_url": "Using",
|
"using_url": "استخدام",
|
||||||
"local": "Local URL",
|
"local": "رابط محلي",
|
||||||
"remote": "Remote URL",
|
"remote": "الـ URL الخارجي",
|
||||||
"not_connected": "Not connected",
|
"not_connected": "غير متصل",
|
||||||
"current_server": "Current Server",
|
"current_server": "الخادم الحالي",
|
||||||
"remote_url": "Remote URL",
|
"remote_url": "الـ URL الخارجي",
|
||||||
"active_url": "Active URL",
|
"active_url": "الرابط النشط",
|
||||||
"not_configured": "Not configured",
|
"not_configured": "لم يتم تكوينه",
|
||||||
"network_added": "Network added",
|
"network_added": "تمت إضافة الشبكة",
|
||||||
"network_already_added": "Network already added",
|
"network_already_added": "الشبكة مضافة مسبقاً",
|
||||||
"no_wifi_connected": "Not connected to WiFi",
|
"no_wifi_connected": "غير متصل بشبكة WiFi",
|
||||||
"permission_denied": "Location permission denied",
|
"permission_denied": "تم رفض إذن الوصول إلى الموقع",
|
||||||
"permission_denied_explanation": "Location permission is required to detect WiFi network for auto-switching. Please enable it in Settings."
|
"permission_denied_explanation": "يتطلب التعرف على شبكة WiFi للتبديل التلقائي الحصول على إذن الوصول إلى الموقع. يرجى تفعيله من الإعدادات."
|
||||||
},
|
},
|
||||||
"user_info": {
|
"user_info": {
|
||||||
"user_info_title": "معلومات المستخدم",
|
"user_info_title": "معلومات المستخدم",
|
||||||
@@ -174,6 +199,22 @@
|
|||||||
"rewind_length": "مدة الترجيع",
|
"rewind_length": "مدة الترجيع",
|
||||||
"seconds_unit": "ث"
|
"seconds_unit": "ث"
|
||||||
},
|
},
|
||||||
|
"buffer": {
|
||||||
|
"title": "Buffer Settings",
|
||||||
|
"cache_mode": "Cache Mode",
|
||||||
|
"cache_auto": "Auto",
|
||||||
|
"cache_yes": "Enabled",
|
||||||
|
"cache_no": "Disabled",
|
||||||
|
"buffer_duration": "Buffer Duration",
|
||||||
|
"max_cache_size": "Max Cache Size",
|
||||||
|
"max_backward_cache": "Max Backward Cache"
|
||||||
|
},
|
||||||
|
"vo_driver": {
|
||||||
|
"title": "Video Output",
|
||||||
|
"vo_mode": "VO Driver",
|
||||||
|
"gpu_next": "gpu-next (Recommended)",
|
||||||
|
"gpu": "gpu"
|
||||||
|
},
|
||||||
"gesture_controls": {
|
"gesture_controls": {
|
||||||
"gesture_controls_title": "التحكم بالإيماءات",
|
"gesture_controls_title": "التحكم بالإيماءات",
|
||||||
"horizontal_swipe_skip": "السحب الأفقي للتخطي",
|
"horizontal_swipe_skip": "السحب الأفقي للتخطي",
|
||||||
@@ -182,10 +223,10 @@
|
|||||||
"left_side_brightness_description": "اسحب لأعلى/لأسفل على الجانب الأيسر لضبط السطوع",
|
"left_side_brightness_description": "اسحب لأعلى/لأسفل على الجانب الأيسر لضبط السطوع",
|
||||||
"right_side_volume": "التحكم في مستوى الصوت من الجانب الأيمن",
|
"right_side_volume": "التحكم في مستوى الصوت من الجانب الأيمن",
|
||||||
"right_side_volume_description": "اسحب لأعلى/لأسفل على الجانب الأيمن لضبط مستوى الصوت",
|
"right_side_volume_description": "اسحب لأعلى/لأسفل على الجانب الأيمن لضبط مستوى الصوت",
|
||||||
"hide_volume_slider": "Hide Volume Slider",
|
"hide_volume_slider": "إخفاء شريط مستوى الصوت",
|
||||||
"hide_volume_slider_description": "Hide the volume slider in the video player",
|
"hide_volume_slider_description": "إخفاء شريط التحكم في مستوى الصوت في مشغل الفيديو",
|
||||||
"hide_brightness_slider": "Hide Brightness Slider",
|
"hide_brightness_slider": "إخفاء شريط السطوع",
|
||||||
"hide_brightness_slider_description": "Hide the brightness slider in the video player"
|
"hide_brightness_slider_description": "إخفاء شريط التحكم في السطوع في مشغل الفيديو"
|
||||||
},
|
},
|
||||||
"audio": {
|
"audio": {
|
||||||
"audio_title": "الصوت",
|
"audio_title": "الصوت",
|
||||||
@@ -195,12 +236,12 @@
|
|||||||
"none": "لا شيء",
|
"none": "لا شيء",
|
||||||
"language": "اللغة",
|
"language": "اللغة",
|
||||||
"transcode_mode": {
|
"transcode_mode": {
|
||||||
"title": "Audio Transcoding",
|
"title": "تحويل ترميز الصوت",
|
||||||
"description": "Controls how surround audio (7.1, TrueHD, DTS-HD) is handled",
|
"description": "يتحكم في كيفية التعامل مع الصوت المحيطي (7.1، TrueHD، DTS-HD)",
|
||||||
"auto": "Auto",
|
"auto": "تلقائي",
|
||||||
"stereo": "Force Stereo",
|
"stereo": "إجبار تشغيل ستيريو",
|
||||||
"5_1": "Allow 5.1",
|
"5_1": "السماح بـ 5.1",
|
||||||
"passthrough": "Passthrough"
|
"passthrough": "تمرير الصوت"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"subtitles": {
|
"subtitles": {
|
||||||
@@ -251,29 +292,45 @@
|
|||||||
"Normal": "عادي",
|
"Normal": "عادي",
|
||||||
"Thick": "سميك"
|
"Thick": "سميك"
|
||||||
},
|
},
|
||||||
"subtitle_color": "Subtitle Color",
|
"subtitle_color": "لون الترجمة",
|
||||||
"subtitle_background_color": "Background Color",
|
"subtitle_background_color": "لون الخلفية",
|
||||||
"subtitle_font": "Subtitle Font",
|
"subtitle_font": "خط الترجمة",
|
||||||
"ksplayer_title": "KSPlayer Settings",
|
"ksplayer_title": "إعدادات KSPlayer",
|
||||||
"hardware_decode": "Hardware Decoding",
|
"hardware_decode": "فك الترميز بواسطة الجهاز",
|
||||||
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
|
"hardware_decode_description": "استخدم تسريع العتاد لفك ترميز الفيديو. قم بتعطيله إذا واجهت مشكلات في التشغيل.",
|
||||||
|
"opensubtitles_title": "OpenSubtitles",
|
||||||
|
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
|
||||||
|
"opensubtitles_api_key": "API Key",
|
||||||
|
"opensubtitles_api_key_placeholder": "Enter API key...",
|
||||||
|
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
|
||||||
|
"mpv_subtitle_scale": "Subtitle Scale",
|
||||||
|
"mpv_subtitle_margin_y": "Vertical Margin",
|
||||||
|
"mpv_subtitle_align_x": "Horizontal Align",
|
||||||
|
"mpv_subtitle_align_y": "Vertical Align",
|
||||||
|
"align": {
|
||||||
|
"left": "Left",
|
||||||
|
"center": "Center",
|
||||||
|
"right": "Right",
|
||||||
|
"top": "Top",
|
||||||
|
"bottom": "Bottom"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"vlc_subtitles": {
|
"vlc_subtitles": {
|
||||||
"title": "VLC Subtitle Settings",
|
"title": "إعدادات ترجمة VLC",
|
||||||
"hint": "Customize subtitle appearance for VLC player. Changes take effect on next playback.",
|
"hint": "تخصيص مظهر الترجمة لمشغل VLC. تصبح التغييرات سارية المفعول عند التشغيل التالي.",
|
||||||
"text_color": "Text Color",
|
"text_color": "لون النص",
|
||||||
"background_color": "Background Color",
|
"background_color": "لون الخلفية",
|
||||||
"background_opacity": "Background Opacity",
|
"background_opacity": "شفافية الخلفية",
|
||||||
"outline_color": "Outline Color",
|
"outline_color": "لون إطار الخط",
|
||||||
"outline_opacity": "Outline Opacity",
|
"outline_opacity": "شفافية إطار الخط",
|
||||||
"outline_thickness": "Outline Thickness",
|
"outline_thickness": "سمك إطار الخط",
|
||||||
"bold": "Bold Text",
|
"bold": "خط عريض",
|
||||||
"margin": "Bottom Margin"
|
"margin": "الهامش السفلي"
|
||||||
},
|
},
|
||||||
"video_player": {
|
"video_player": {
|
||||||
"title": "Video Player",
|
"title": "مشغل الفيديو",
|
||||||
"video_player": "Video Player",
|
"video_player": "مشغل الفيديو",
|
||||||
"video_player_description": "Choose which video player to use on iOS.",
|
"video_player_description": "اختر مشغل الفيديو الذي سيتم استخدامه على نظام iOS.",
|
||||||
"ksplayer": "KSPlayer",
|
"ksplayer": "KSPlayer",
|
||||||
"vlc": "VLC"
|
"vlc": "VLC"
|
||||||
},
|
},
|
||||||
@@ -305,8 +362,8 @@
|
|||||||
"select_liraries_you_want_to_hide": "اختر المكتبات التي تريد إخفاءها من تبويب المكتبة وأقسام الصفحة الرئيسية.",
|
"select_liraries_you_want_to_hide": "اختر المكتبات التي تريد إخفاءها من تبويب المكتبة وأقسام الصفحة الرئيسية.",
|
||||||
"disable_haptic_feedback": "تعطيل ردود الفعل اللمسية",
|
"disable_haptic_feedback": "تعطيل ردود الفعل اللمسية",
|
||||||
"default_quality": "الجودة الافتراضية",
|
"default_quality": "الجودة الافتراضية",
|
||||||
"default_playback_speed": "Default Playback Speed",
|
"default_playback_speed": "سرعة التشغيل الافتراضية",
|
||||||
"auto_play_next_episode": "Auto-play Next Episode",
|
"auto_play_next_episode": "تشغيل الحلقة التالية تلقائياً",
|
||||||
"max_auto_play_episode_count": "الحد الأقصى لعدد الحلقات التي يتم تشغيلها تلقائيًا",
|
"max_auto_play_episode_count": "الحد الأقصى لعدد الحلقات التي يتم تشغيلها تلقائيًا",
|
||||||
"disabled": "معطل"
|
"disabled": "معطل"
|
||||||
},
|
},
|
||||||
@@ -314,15 +371,15 @@
|
|||||||
"downloads_title": "التنزيلات"
|
"downloads_title": "التنزيلات"
|
||||||
},
|
},
|
||||||
"music": {
|
"music": {
|
||||||
"title": "Music",
|
"title": "الموسيقى",
|
||||||
"playback_title": "Playback",
|
"playback_title": "التشغيل",
|
||||||
"playback_description": "Configure how music is played.",
|
"playback_description": "ضبط كيفية تشغيل الموسيقى.",
|
||||||
"prefer_downloaded": "Prefer Downloaded Songs",
|
"prefer_downloaded": "تفضيل الأغاني التي تم تنزيلها",
|
||||||
"caching_title": "Caching",
|
"caching_title": "التخزين المؤقت",
|
||||||
"caching_description": "Automatically cache upcoming tracks for smoother playback.",
|
"caching_description": "تخزين الأغاني التالية مؤقتاً تلقائياً لضمان تشغيل أكثر سلاسة.",
|
||||||
"lookahead_enabled": "Enable Look-Ahead Caching",
|
"lookahead_enabled": "تفعيل التخزين المؤقت الاستباقي",
|
||||||
"lookahead_count": "Tracks to Pre-cache",
|
"lookahead_count": "عدد الأغاني المراد تخزينها مسبقاً",
|
||||||
"max_cache_size": "Max Cache Size"
|
"max_cache_size": "الحد الأقصى لحجم التخزين المؤقت"
|
||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"plugins_title": "الإضافات",
|
"plugins_title": "الإضافات",
|
||||||
@@ -357,39 +414,39 @@
|
|||||||
"save_button": "حفظ",
|
"save_button": "حفظ",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"saved": "تم الحفظ",
|
"saved": "تم الحفظ",
|
||||||
"refreshed": "Settings refreshed from server"
|
"refreshed": "تم تحديث الإعدادات من الخادم"
|
||||||
},
|
},
|
||||||
"refresh_from_server": "Refresh Settings from Server"
|
"refresh_from_server": "تحديث الإعدادات من الخادم"
|
||||||
},
|
},
|
||||||
"streamystats": {
|
"streamystats": {
|
||||||
"enable_streamystats": "Enable Streamystats",
|
"enable_streamystats": "تفعيل Streamystats",
|
||||||
"disable_streamystats": "Disable Streamystats",
|
"disable_streamystats": "تعطيل Streamystats",
|
||||||
"enable_search": "Use for Search",
|
"enable_search": "استخدم للبحث",
|
||||||
"url": "URL",
|
"url": "الرابط",
|
||||||
"server_url_placeholder": "http(s)://streamystats.example.com",
|
"server_url_placeholder": "http(s)://streamystats.example.com",
|
||||||
"streamystats_search_hint": "Enter the URL for your Streamystats server. The URL should include http or https and optionally the port.",
|
"streamystats_search_hint": "أدخل رابط خادم Streamystats الخاص بك. يجب أن يتضمن الرابط البروتوكول http أو https مع رقم المنفذ اختيارياً.",
|
||||||
"read_more_about_streamystats": "Read More About Streamystats.",
|
"read_more_about_streamystats": "اقرأ المزيد عن Streamystats.",
|
||||||
"save_button": "Save",
|
"save_button": "حفظ",
|
||||||
"save": "Save",
|
"save": "حفظ",
|
||||||
"features_title": "Features",
|
"features_title": "المميزات",
|
||||||
"home_sections_title": "Home Sections",
|
"home_sections_title": "أقسام الرئيسية",
|
||||||
"enable_movie_recommendations": "Movie Recommendations",
|
"enable_movie_recommendations": "توصيات الأفلام",
|
||||||
"enable_series_recommendations": "Series Recommendations",
|
"enable_series_recommendations": "توصيات المسلسلات",
|
||||||
"enable_promoted_watchlists": "Promoted Watchlists",
|
"enable_promoted_watchlists": "قوائم مشاهدة مختارة",
|
||||||
"hide_watchlists_tab": "Hide Watchlists Tab",
|
"hide_watchlists_tab": "إخفاء تبويب قوائم المشاهدة",
|
||||||
"home_sections_hint": "Show personalized recommendations and promoted watchlists from Streamystats on the home page.",
|
"home_sections_hint": "إظهار التوصيات المخصصة وقوائم المشاهدة المختارة من Streamystats في الصفحة الرئيسية.",
|
||||||
"recommended_movies": "Recommended Movies",
|
"recommended_movies": "أفلام موصى بها",
|
||||||
"recommended_series": "Recommended Series",
|
"recommended_series": "مسلسلات موصى بها",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"saved": "Saved",
|
"saved": "تم الحفظ",
|
||||||
"refreshed": "Settings refreshed from server",
|
"refreshed": "تم تحديث الإعدادات من الخادم",
|
||||||
"disabled": "Streamystats disabled"
|
"disabled": "تم تعطيل Streamystats"
|
||||||
},
|
},
|
||||||
"refresh_from_server": "Refresh Settings from Server"
|
"refresh_from_server": "تحديث الإعدادات من الخادم"
|
||||||
},
|
},
|
||||||
"kefinTweaks": {
|
"kefinTweaks": {
|
||||||
"watchlist_enabler": "Enable our Watchlist integration",
|
"watchlist_enabler": "تفعيل الربط مع قائمة المشاهدة الخاصة بنا",
|
||||||
"watchlist_button": "Toggle Watchlist integration"
|
"watchlist_button": "تبديل حالة ربط قائمة المشاهدة"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"storage": {
|
"storage": {
|
||||||
@@ -398,15 +455,21 @@
|
|||||||
"device_usage": "الجهاز {{availableSpace}}%",
|
"device_usage": "الجهاز {{availableSpace}}%",
|
||||||
"size_used": "تم استخدام {{used}} من {{total}}",
|
"size_used": "تم استخدام {{used}} من {{total}}",
|
||||||
"delete_all_downloaded_files": "حذف جميع الملفات التي تم تنزيلها",
|
"delete_all_downloaded_files": "حذف جميع الملفات التي تم تنزيلها",
|
||||||
"music_cache_title": "Music Cache",
|
"music_cache_title": "التخزين المؤقت للموسيقى",
|
||||||
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
|
"music_cache_description": "تخزين الأغاني تلقائياً أثناء الاستماع لضمان تشغيل أكثر سلاسة ودعم الاستماع بدون اتصال",
|
||||||
"enable_music_cache": "Enable Music Cache",
|
"enable_music_cache": "تمكين التخزين المؤقت للموسيقى",
|
||||||
"clear_music_cache": "Clear Music Cache",
|
"clear_music_cache": "مسح التخزين المؤقت للموسيقى",
|
||||||
"music_cache_size": "{{size}} cached",
|
"music_cache_size": "تم تخزين {{size}} مؤقتاً",
|
||||||
"music_cache_cleared": "Music cache cleared",
|
"music_cache_cleared": "تم مسح التخزين المؤقت للموسيقى",
|
||||||
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
"delete_all_downloaded_songs": "حذف جميع الأغاني التي تم تنزيلها",
|
||||||
"downloaded_songs_size": "{{size}} downloaded",
|
"downloaded_songs_size": "تم تنزيل {{size}}",
|
||||||
"downloaded_songs_deleted": "Downloaded songs deleted"
|
"downloaded_songs_deleted": "تم حذف الأغاني التي تم تنزيلها",
|
||||||
|
"clear_all_cache": "Clear All Cache",
|
||||||
|
"clear_all_cache_confirm": "Clear All Cache?",
|
||||||
|
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
|
||||||
|
"clear_all_cache_success": "Cache Cleared",
|
||||||
|
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
|
||||||
|
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "المقدمة",
|
"title": "المقدمة",
|
||||||
@@ -430,6 +493,21 @@
|
|||||||
"error_deleting_files": "خطأ في حذف الملفات",
|
"error_deleting_files": "خطأ في حذف الملفات",
|
||||||
"background_downloads_enabled": "تم تفعيل التنزيلات في الخلفية",
|
"background_downloads_enabled": "تم تفعيل التنزيلات في الخلفية",
|
||||||
"background_downloads_disabled": "تم تعطيل التنزيلات في الخلفية"
|
"background_downloads_disabled": "تم تعطيل التنزيلات في الخلفية"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"title": "Security",
|
||||||
|
"inactivity_timeout": {
|
||||||
|
"title": "Inactivity Timeout",
|
||||||
|
"description": "Auto logout after inactivity",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"1_minute": "1 minute",
|
||||||
|
"5_minutes": "5 minutes",
|
||||||
|
"15_minutes": "15 minutes",
|
||||||
|
"30_minutes": "30 minutes",
|
||||||
|
"1_hour": "1 hour",
|
||||||
|
"4_hours": "4 hours",
|
||||||
|
"24_hours": "24 hours"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -456,6 +534,7 @@
|
|||||||
"new_app_version_requires_re_download_description": "يتطلب التحديث الجديد تنزيل المحتوى مرة أخرى. يرجى إزالة كل المحتوى الذي تم تنزيله والمحاولة مرة أخرى.",
|
"new_app_version_requires_re_download_description": "يتطلب التحديث الجديد تنزيل المحتوى مرة أخرى. يرجى إزالة كل المحتوى الذي تم تنزيله والمحاولة مرة أخرى.",
|
||||||
"back": "رجوع",
|
"back": "رجوع",
|
||||||
"delete": "حذف",
|
"delete": "حذف",
|
||||||
|
"delete_download": "Delete Download",
|
||||||
"something_went_wrong": "حدث خطأ ما",
|
"something_went_wrong": "حدث خطأ ما",
|
||||||
"could_not_get_stream_url_from_jellyfin": "تعذر الحصول على رابط البث من Jellyfin",
|
"could_not_get_stream_url_from_jellyfin": "تعذر الحصول على رابط البث من Jellyfin",
|
||||||
"eta": "الوقت المتبقي {{eta}}",
|
"eta": "الوقت المتبقي {{eta}}",
|
||||||
@@ -492,22 +571,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"no_results": "No Results",
|
||||||
"select": "اختر",
|
"select": "اختر",
|
||||||
"no_trailer_available": "لا يوجد مقطع دعائي متوفر",
|
"no_trailer_available": "لا يوجد مقطع دعائي متوفر",
|
||||||
"video": "فيديو",
|
"video": "فيديو",
|
||||||
"audio": "الصوت",
|
"audio": "الصوت",
|
||||||
"subtitle": "الترجمة",
|
"subtitle": "الترجمة",
|
||||||
"play": "تشغيل",
|
"play": "تشغيل",
|
||||||
|
"mark_as_played": "Mark as Played",
|
||||||
|
"mark_as_not_played": "Mark as not Played",
|
||||||
"none": "لا شيء",
|
"none": "لا شيء",
|
||||||
"track": "Track",
|
"track": "أغنية",
|
||||||
"cancel": "Cancel",
|
"cancel": "إلغاء",
|
||||||
"delete": "Delete",
|
"stop": "Stop",
|
||||||
"ok": "OK",
|
"delete": "حذف",
|
||||||
"remove": "Remove",
|
"ok": "حسناً",
|
||||||
"next": "Next",
|
"remove": "إزالة",
|
||||||
"back": "Back",
|
"next": "التالي",
|
||||||
"continue": "Continue",
|
"back": "رجوع",
|
||||||
"verifying": "Verifying..."
|
"continue": "متابعة",
|
||||||
|
"verifying": "جارٍ التحقق...",
|
||||||
|
"login": "Login",
|
||||||
|
"refresh": "Refresh"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"search": "بحث...",
|
"search": "بحث...",
|
||||||
@@ -521,10 +606,10 @@
|
|||||||
"episodes": "حلقات",
|
"episodes": "حلقات",
|
||||||
"collections": "مجموعات",
|
"collections": "مجموعات",
|
||||||
"actors": "ممثلون",
|
"actors": "ممثلون",
|
||||||
"artists": "Artists",
|
"artists": "الفنانون",
|
||||||
"albums": "Albums",
|
"albums": "الألبومات",
|
||||||
"songs": "Songs",
|
"songs": "الأغاني",
|
||||||
"playlists": "Playlists",
|
"playlists": "قوائم التشغيل",
|
||||||
"request_movies": "طلب أفلام",
|
"request_movies": "طلب أفلام",
|
||||||
"request_series": "طلب مسلسلات",
|
"request_series": "طلب مسلسلات",
|
||||||
"recently_added": "أضيف مؤخرًا",
|
"recently_added": "أضيف مؤخرًا",
|
||||||
@@ -556,6 +641,7 @@
|
|||||||
"movies": "أفلام",
|
"movies": "أفلام",
|
||||||
"series": "مسلسلات",
|
"series": "مسلسلات",
|
||||||
"boxsets": "مجموعات",
|
"boxsets": "مجموعات",
|
||||||
|
"playlists": "Playlists",
|
||||||
"items": "عناصر"
|
"items": "عناصر"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
@@ -566,15 +652,20 @@
|
|||||||
"poster": "ملصق",
|
"poster": "ملصق",
|
||||||
"cover": "غلاف",
|
"cover": "غلاف",
|
||||||
"show_titles": "إظهار العناوين",
|
"show_titles": "إظهار العناوين",
|
||||||
"show_stats": "إظهار الإحصائيات"
|
"show_stats": "إظهار الإحصائيات",
|
||||||
|
"options_title": "Options"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"genres": "الأنواع",
|
"genres": "الأنواع",
|
||||||
"years": "السنوات",
|
"years": "السنوات",
|
||||||
"sort_by": "ترتيب حسب",
|
"sort_by": "ترتيب حسب",
|
||||||
"filter_by": "Filter By",
|
"filter_by": "تصفية حسب",
|
||||||
"sort_order": "اتجاه الترتيب",
|
"sort_order": "اتجاه الترتيب",
|
||||||
"tags": "الوسوم"
|
"tags": "الوسوم",
|
||||||
|
"all": "All",
|
||||||
|
"reset": "Reset",
|
||||||
|
"asc": "Ascending",
|
||||||
|
"desc": "Descending"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"favorites": {
|
"favorites": {
|
||||||
@@ -591,6 +682,8 @@
|
|||||||
"no_links": "لا توجد روابط"
|
"no_links": "لا توجد روابط"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
|
"live": "LIVE",
|
||||||
|
"mpv_player_title": "MPV Player",
|
||||||
"error": "خطأ",
|
"error": "خطأ",
|
||||||
"failed_to_get_stream_url": "فشل في الحصول على رابط البث",
|
"failed_to_get_stream_url": "فشل في الحصول على رابط البث",
|
||||||
"an_error_occured_while_playing_the_video": "حدث خطأ أثناء تشغيل الفيديو. تحقق من السجلات في الإعدادات.",
|
"an_error_occured_while_playing_the_video": "حدث خطأ أثناء تشغيل الفيديو. تحقق من السجلات في الإعدادات.",
|
||||||
@@ -604,11 +697,39 @@
|
|||||||
"index": "الفِهْرِس:",
|
"index": "الفِهْرِس:",
|
||||||
"continue_watching": "متابعة المشاهدة",
|
"continue_watching": "متابعة المشاهدة",
|
||||||
"go_back": "رجوع",
|
"go_back": "رجوع",
|
||||||
"downloaded_file_title": "You have this file downloaded",
|
"downloaded_file_title": "تم تنزيل هذا الملف",
|
||||||
"downloaded_file_message": "هل تريد تشغيل الملف الذي تم تنزيله؟",
|
"downloaded_file_message": "هل تريد تشغيل الملف الذي تم تنزيله؟",
|
||||||
"downloaded_file_yes": "نعم",
|
"downloaded_file_yes": "نعم",
|
||||||
"downloaded_file_no": "لا",
|
"downloaded_file_no": "لا",
|
||||||
"downloaded_file_cancel": "Cancel"
|
"downloaded_file_cancel": "إلغاء",
|
||||||
|
"swipe_down_settings": "Swipe down for settings",
|
||||||
|
"ends_at": "Ends at {{time}}",
|
||||||
|
"search_subtitles": "Search Subtitles",
|
||||||
|
"subtitle_tracks": "Tracks",
|
||||||
|
"subtitle_search": "Search & Download",
|
||||||
|
"download": "Download",
|
||||||
|
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
|
||||||
|
"using_jellyfin_server": "Using Jellyfin Server",
|
||||||
|
"language": "Language",
|
||||||
|
"results": "Results",
|
||||||
|
"searching": "Searching...",
|
||||||
|
"search_failed": "Search failed",
|
||||||
|
"no_subtitle_provider": "No subtitle provider configured on server",
|
||||||
|
"no_subtitles_found": "No subtitles found",
|
||||||
|
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
|
||||||
|
"settings": "Settings",
|
||||||
|
"skip_intro": "Skip Intro",
|
||||||
|
"skip_credits": "Skip Credits",
|
||||||
|
"stopPlayback": "Stop Playback",
|
||||||
|
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
|
||||||
|
"stopPlayingConfirm": "Are you sure you want to stop playback?",
|
||||||
|
"downloaded": "Downloaded"
|
||||||
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "التالي",
|
"next_up": "التالي",
|
||||||
@@ -617,6 +738,11 @@
|
|||||||
"series": "مسلسلات",
|
"series": "مسلسلات",
|
||||||
"seasons": "مواسم",
|
"seasons": "مواسم",
|
||||||
"season": "موسم",
|
"season": "موسم",
|
||||||
|
"from_this_series": "From This Series",
|
||||||
|
"more_from_this_season": "More from this Season",
|
||||||
|
"view_series": "View Series",
|
||||||
|
"view_season": "View Season",
|
||||||
|
"select_season": "Select Season",
|
||||||
"no_episodes_for_this_season": "لا توجد حلقات لهذا الموسم",
|
"no_episodes_for_this_season": "لا توجد حلقات لهذا الموسم",
|
||||||
"overview": "نظرة عامة",
|
"overview": "نظرة عامة",
|
||||||
"more_with": "المزيد مع {{name}}",
|
"more_with": "المزيد مع {{name}}",
|
||||||
@@ -624,13 +750,24 @@
|
|||||||
"no_similar_items_found": "لم يتم العثور على عناصر مشابهة",
|
"no_similar_items_found": "لم يتم العثور على عناصر مشابهة",
|
||||||
"video": "فيديو",
|
"video": "فيديو",
|
||||||
"more_details": "المزيد من التفاصيل",
|
"more_details": "المزيد من التفاصيل",
|
||||||
"media_options": "Media Options",
|
"media_options": "خيارات الوسائط",
|
||||||
"quality": "الجودة",
|
"quality": "الجودة",
|
||||||
"audio": "الصوت",
|
"audio": "الصوت",
|
||||||
"subtitles": "الترجمة",
|
"subtitles": {
|
||||||
|
"label": "Subtitle",
|
||||||
|
"none": "None",
|
||||||
|
"tracks": "Tracks"
|
||||||
|
},
|
||||||
"show_more": "عرض المزيد",
|
"show_more": "عرض المزيد",
|
||||||
"show_less": "عرض أقل",
|
"show_less": "عرض أقل",
|
||||||
|
"left": "left",
|
||||||
|
"more_info": "More Info",
|
||||||
|
"director": "Director",
|
||||||
|
"cast": "Cast",
|
||||||
|
"technical_details": "Technical Details",
|
||||||
"appeared_in": "ظهر في",
|
"appeared_in": "ظهر في",
|
||||||
|
"movies": "Movies",
|
||||||
|
"shows": "Shows",
|
||||||
"could_not_load_item": "تعذر تحميل العنصر",
|
"could_not_load_item": "تعذر تحميل العنصر",
|
||||||
"none": "لا شيء",
|
"none": "لا شيء",
|
||||||
"download": {
|
"download": {
|
||||||
@@ -641,7 +778,13 @@
|
|||||||
"download_x_item": "تنزيل {{item_count}} عناصر",
|
"download_x_item": "تنزيل {{item_count}} عناصر",
|
||||||
"download_unwatched_only": "غير المشاهدة فقط",
|
"download_unwatched_only": "غير المشاهدة فقط",
|
||||||
"download_button": "تنزيل"
|
"download_button": "تنزيل"
|
||||||
}
|
},
|
||||||
|
"mark_played": "Mark as Watched",
|
||||||
|
"mark_unplayed": "Mark as Unwatched",
|
||||||
|
"resume_playback": "Resume Playback",
|
||||||
|
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
|
||||||
|
"play_from_start": "Play from Start",
|
||||||
|
"continue_from": "Continue from {{time}}"
|
||||||
},
|
},
|
||||||
"live_tv": {
|
"live_tv": {
|
||||||
"next": "التالي",
|
"next": "التالي",
|
||||||
@@ -652,7 +795,18 @@
|
|||||||
"movies": "أفلام",
|
"movies": "أفلام",
|
||||||
"sports": "رياضة",
|
"sports": "رياضة",
|
||||||
"for_kids": "للأطفال",
|
"for_kids": "للأطفال",
|
||||||
"news": "أخبار"
|
"news": "أخبار",
|
||||||
|
"page_of": "Page {{current}} of {{total}}",
|
||||||
|
"no_programs": "No programs available",
|
||||||
|
"no_channels": "No channels available",
|
||||||
|
"tabs": {
|
||||||
|
"programs": "Programs",
|
||||||
|
"guide": "Guide",
|
||||||
|
"channels": "Channels",
|
||||||
|
"recordings": "Recordings",
|
||||||
|
"schedule": "Schedule",
|
||||||
|
"series": "Series"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"jellyseerr": {
|
"jellyseerr": {
|
||||||
"confirm": "تأكيد",
|
"confirm": "تأكيد",
|
||||||
@@ -697,6 +851,12 @@
|
|||||||
"decline": "رفض",
|
"decline": "رفض",
|
||||||
"requested_by": "مطلوب من {{user}}",
|
"requested_by": "مطلوب من {{user}}",
|
||||||
"unknown_user": "مستخدم غير معروف",
|
"unknown_user": "مستخدم غير معروف",
|
||||||
|
"select": "Select",
|
||||||
|
"request_all": "Request All",
|
||||||
|
"request_seasons": "Request Seasons",
|
||||||
|
"select_seasons": "Select Seasons",
|
||||||
|
"request_selected": "Request Selected",
|
||||||
|
"n_selected": "{{count}} selected",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"jellyseer_does_not_meet_requirements": "خادم Seerr لا يستوفي الحد الأدنى للإصدار المطلوب! يرجى التحديث إلى إصدار 2.0.0 على الأقل",
|
"jellyseer_does_not_meet_requirements": "خادم Seerr لا يستوفي الحد الأدنى للإصدار المطلوب! يرجى التحديث إلى إصدار 2.0.0 على الأقل",
|
||||||
"jellyseerr_test_failed": "فشل اختبار Seerr. يرجى المحاولة مرة أخرى.",
|
"jellyseerr_test_failed": "فشل اختبار Seerr. يرجى المحاولة مرة أخرى.",
|
||||||
@@ -716,130 +876,162 @@
|
|||||||
"search": "بحث",
|
"search": "بحث",
|
||||||
"library": "المكتبة",
|
"library": "المكتبة",
|
||||||
"custom_links": "روابط مخصصة",
|
"custom_links": "روابط مخصصة",
|
||||||
"favorites": "المفضلة"
|
"favorites": "المفضلة",
|
||||||
|
"settings": "Settings"
|
||||||
},
|
},
|
||||||
"music": {
|
"music": {
|
||||||
"title": "Music",
|
"title": "الموسيقى",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"suggestions": "Suggestions",
|
"suggestions": "الإقتراحات",
|
||||||
"albums": "Albums",
|
"albums": "الألبومات",
|
||||||
"artists": "Artists",
|
"artists": "الفنانون",
|
||||||
"playlists": "Playlists",
|
"playlists": "قوائم التشغيل",
|
||||||
"tracks": "tracks"
|
"tracks": "الأغاني"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"all": "All"
|
"all": "الكل"
|
||||||
},
|
},
|
||||||
"recently_added": "Recently Added",
|
"recently_added": "أضيف مؤخرًا",
|
||||||
"recently_played": "Recently Played",
|
"recently_played": "تم تشغيله مؤخرًا",
|
||||||
"frequently_played": "Frequently Played",
|
"frequently_played": "الأكثر تشغيلاً",
|
||||||
"explore": "Explore",
|
"explore": "اكتشف",
|
||||||
"top_tracks": "Top Tracks",
|
"top_tracks": "أفضل الأغاني",
|
||||||
"play": "Play",
|
"play": "تشغيل",
|
||||||
"shuffle": "Shuffle",
|
"shuffle": "ترتيب عشوائي",
|
||||||
"play_top_tracks": "Play Top Tracks",
|
"play_top_tracks": "تشغيل أفضل الأغاني",
|
||||||
"no_suggestions": "No suggestions available",
|
"no_suggestions": "لا توجد مقترحات متاحة",
|
||||||
"no_albums": "No albums found",
|
"no_albums": "لا توجد ألبومات",
|
||||||
"no_artists": "No artists found",
|
"no_artists": "لا يوجد فنانون",
|
||||||
"no_playlists": "No playlists found",
|
"no_playlists": "لا توجد قوائم تشغيل",
|
||||||
"album_not_found": "Album not found",
|
"album_not_found": "الألبوم غير موجود",
|
||||||
"artist_not_found": "Artist not found",
|
"artist_not_found": "الفنان غير موجود",
|
||||||
"playlist_not_found": "Playlist not found",
|
"playlist_not_found": "قائمة التشغيل غير موجودة",
|
||||||
"track_options": {
|
"track_options": {
|
||||||
"play_next": "Play Next",
|
"play_next": "تشغيل التالي",
|
||||||
"add_to_queue": "Add to Queue",
|
"add_to_queue": "إضافة إلى قائمة الانتظار",
|
||||||
"add_to_playlist": "Add to Playlist",
|
"add_to_playlist": "أضف إلى قائمة التشغيل",
|
||||||
"download": "Download",
|
"download": "تنزيل",
|
||||||
"downloaded": "Downloaded",
|
"downloaded": "تم التنزيل",
|
||||||
"downloading": "Downloading...",
|
"downloading": "جارٍ التنزيل...",
|
||||||
"cached": "Cached",
|
"cached": "تم التخزين مؤقتاً",
|
||||||
"delete_download": "Delete Download",
|
"delete_download": "حذف ملف التنزيل",
|
||||||
"delete_cache": "Remove from Cache",
|
"delete_cache": "إزالة من التخزين المؤقت",
|
||||||
"go_to_artist": "Go to Artist",
|
"go_to_artist": "انتقال إلى الفنان",
|
||||||
"go_to_album": "Go to Album",
|
"go_to_album": "انتقال إلى الألبوم",
|
||||||
"add_to_favorites": "Add to Favorites",
|
"add_to_favorites": "إضافة إلى المفضلة",
|
||||||
"remove_from_favorites": "Remove from Favorites",
|
"remove_from_favorites": "إزالة من المفضلة",
|
||||||
"remove_from_playlist": "Remove from Playlist"
|
"remove_from_playlist": "إزالة من قائمة التشغيل"
|
||||||
},
|
},
|
||||||
"playlists": {
|
"playlists": {
|
||||||
"create_playlist": "Create Playlist",
|
"create_playlist": "إنشاء قائمة التشغيل",
|
||||||
"playlist_name": "Playlist Name",
|
"playlist_name": "اسم قائمة التشغيل",
|
||||||
"enter_name": "Enter playlist name",
|
"enter_name": "أدخل اسم قائمة التشغيل",
|
||||||
"create": "Create",
|
"create": "إنشاء",
|
||||||
"search_playlists": "Search playlists...",
|
"search_playlists": "البحث عن قوائم التشغيل...",
|
||||||
"added_to": "Added to {{name}}",
|
"added_to": "تمت الإضافة إلى {{name}}",
|
||||||
"added": "Added to playlist",
|
"added": "تمت الإضافة إلى قائمة التشغيل",
|
||||||
"removed_from": "Removed from {{name}}",
|
"removed_from": "تمت الإزالة من {{name}}",
|
||||||
"removed": "Removed from playlist",
|
"removed": "تمت الازالة من قائمة التشغيل",
|
||||||
"created": "Playlist created",
|
"created": "تم إنشاء قائمة التشغيل",
|
||||||
"create_new": "Create New Playlist",
|
"create_new": "إنشاء قائمة تشغيل جديدة",
|
||||||
"failed_to_add": "Failed to add to playlist",
|
"failed_to_add": "فشلت الإضافة إلى قائمة التشغيل",
|
||||||
"failed_to_remove": "Failed to remove from playlist",
|
"failed_to_remove": "فشلت الإزالة من قائمة التشغيل",
|
||||||
"failed_to_create": "Failed to create playlist",
|
"failed_to_create": "فشل إنشاء قائمة التشغيل",
|
||||||
"delete_playlist": "Delete Playlist",
|
"delete_playlist": "حذف قائمة التشغيل",
|
||||||
"delete_confirm": "Are you sure you want to delete \"{{name}}\"? This action cannot be undone.",
|
"delete_confirm": "هل أنت متأكد من رغبتك في حذف {{name}}؟ لا يمكن التراجع عن هذا الإجراء.",
|
||||||
"deleted": "Playlist deleted",
|
"deleted": "تم حذف قائمة التشغيل",
|
||||||
"failed_to_delete": "Failed to delete playlist"
|
"failed_to_delete": "فشل إنشاء قائمة التشغيل"
|
||||||
},
|
},
|
||||||
"sort": {
|
"sort": {
|
||||||
"title": "Sort By",
|
"title": "ترتيب حسب",
|
||||||
"alphabetical": "Alphabetical",
|
"alphabetical": "أبجدي",
|
||||||
"date_created": "Date Created"
|
"date_created": "تاريخ الإنشاء"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"watchlists": {
|
"watchlists": {
|
||||||
"title": "Watchlists",
|
"title": "قوائم المشاهدة",
|
||||||
"my_watchlists": "My Watchlists",
|
"my_watchlists": "قوائم المشاهدة الخاصة بي",
|
||||||
"public_watchlists": "Public Watchlists",
|
"public_watchlists": "قوائم مشاهدة عامة",
|
||||||
"create_title": "Create Watchlist",
|
"create_title": "إنشاء قائمة مشاهدة",
|
||||||
"edit_title": "Edit Watchlist",
|
"edit_title": "تعديل قائمة المشاهدة",
|
||||||
"create_button": "Create Watchlist",
|
"create_button": "إنشاء قائمة مشاهدة",
|
||||||
"save_button": "Save Changes",
|
"save_button": "حفظ التغييرات",
|
||||||
"delete_button": "Delete",
|
"delete_button": "حذف",
|
||||||
"remove_button": "Remove",
|
"remove_button": "إزالة",
|
||||||
"cancel_button": "Cancel",
|
"cancel_button": "إلغاء",
|
||||||
"name_label": "Name",
|
"name_label": "الاسم",
|
||||||
"name_placeholder": "Enter watchlist name",
|
"name_placeholder": "أدخل اسم قائمة المشاهدة",
|
||||||
"description_label": "Description",
|
"description_label": "الوصف",
|
||||||
"description_placeholder": "Enter description (optional)",
|
"description_placeholder": "أدخل الوصف (اختياري)",
|
||||||
"is_public_label": "Public Watchlist",
|
"is_public_label": "قائمة مشاهدة عامة",
|
||||||
"is_public_description": "Allow others to view this watchlist",
|
"is_public_description": "السماح للآخرين بعرض قائمة المشاهدة هذه",
|
||||||
"allowed_type_label": "Content Type",
|
"allowed_type_label": "نوع المحتوى",
|
||||||
"sort_order_label": "Default Sort Order",
|
"sort_order_label": "الترتيب الافتراضي",
|
||||||
"empty_title": "No Watchlists",
|
"empty_title": "لا توجد قوائم مشاهدة",
|
||||||
"empty_description": "Create your first watchlist to start organizing your media",
|
"empty_description": "قم بإنشاء أول قائمة مشاهدة لبدء تنظيم الوسائط الخاصة بك",
|
||||||
"empty_watchlist": "This watchlist is empty",
|
"empty_watchlist": "قائمة المشاهدة هذه فارغة",
|
||||||
"empty_watchlist_hint": "Add items from your library to this watchlist",
|
"empty_watchlist_hint": "إضافة عناصر من مكتبتك إلى قائمة المشاهدة هذه",
|
||||||
"not_configured_title": "Streamystats Not Configured",
|
"not_configured_title": "لم يتم ضبط Streamystats",
|
||||||
"not_configured_description": "Configure Streamystats in settings to use watchlists",
|
"not_configured_description": "اضبط Streamystats في الإعدادات لاستخدام قوائم المشاهدة",
|
||||||
"go_to_settings": "Go to Settings",
|
"go_to_settings": "الذهاب إلى الإعدادات",
|
||||||
"add_to_watchlist": "Add to Watchlist",
|
"add_to_watchlist": "إضافة إلى قائمة المشاهدة",
|
||||||
"remove_from_watchlist": "Remove from Watchlist",
|
"remove_from_watchlist": "إزالة من قائمة المشاهدة",
|
||||||
"select_watchlist": "Select Watchlist",
|
"select_watchlist": "تحديد قائمة المشاهدة",
|
||||||
"create_new": "Create New Watchlist",
|
"create_new": "إنشاء قائمة مشاهدة جديدة",
|
||||||
"item": "item",
|
"item": "عنصر",
|
||||||
"items": "items",
|
"items": "عناصر",
|
||||||
"public": "Public",
|
"public": "عامة",
|
||||||
"private": "Private",
|
"private": "خاصة",
|
||||||
"you": "You",
|
"you": "أنت",
|
||||||
"by_owner": "By another user",
|
"by_owner": "بواسطة مستخدم آخر",
|
||||||
"not_found": "Watchlist not found",
|
"not_found": "قائمة المشاهدة غير موجودة",
|
||||||
"delete_confirm_title": "Delete Watchlist",
|
"delete_confirm_title": "حذف قائمة المشاهدة",
|
||||||
"delete_confirm_message": "Are you sure you want to delete \"{{name}}\"? This action cannot be undone.",
|
"delete_confirm_message": "هل أنت متأكد من رغبتك في حذف \"{{name}}\"؟ لا يمكن التراجع عن هذا الإجراء.",
|
||||||
"remove_item_title": "Remove from Watchlist",
|
"remove_item_title": "إزالة من قائمة المشاهدة",
|
||||||
"remove_item_message": "Remove \"{{name}}\" from this watchlist?",
|
"remove_item_message": "إزالة \"{{name}}\" من قائمة المشاهدة هذه؟",
|
||||||
"loading": "Loading watchlists...",
|
"loading": "تحميل قوائم المشاهدة...",
|
||||||
"no_compatible_watchlists": "No compatible watchlists",
|
"no_compatible_watchlists": "لا توجد قوائم مشاهدة متوافقة",
|
||||||
"create_one_first": "Create a watchlist that accepts this content type"
|
"create_one_first": "إنشاء قائمة مشاهدة تقبل نوع المحتوى هذا"
|
||||||
},
|
},
|
||||||
"playback_speed": {
|
"playback_speed": {
|
||||||
"title": "Playback Speed",
|
"title": "سرعة التشغيل",
|
||||||
"apply_to": "Apply To",
|
"apply_to": "تطبيق على",
|
||||||
"speed": "Speed",
|
"speed": "السرعة",
|
||||||
"scope": {
|
"scope": {
|
||||||
"media": "This media only",
|
"media": "الوسائط هذه فقط",
|
||||||
"show": "This show",
|
"show": "هذا المسلسل",
|
||||||
"all": "All media (default)"
|
"all": "جميع الوسائط (الافتراضي)"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"companion_login": {
|
||||||
|
"title": "Pair with TV",
|
||||||
|
"align_qr": "Align the QR code within the frame",
|
||||||
|
"enter_code_manually": "Enter code manually",
|
||||||
|
"pairing_enter_credentials": "Enter credentials for TV",
|
||||||
|
"pairing_code_label": "Pairing code",
|
||||||
|
"server": "Server",
|
||||||
|
"authorize_button": "Authorize",
|
||||||
|
"authorizing": "Authorizing...",
|
||||||
|
"scan_again": "Scan Again",
|
||||||
|
"done": "Done",
|
||||||
|
"success_title": "Authorization Sent",
|
||||||
|
"pairing_tv_connecting": "The TV is connecting to your account",
|
||||||
|
"error_title": "Authorization Failed",
|
||||||
|
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
|
||||||
|
"error_generic": "Something went wrong. Please try again.",
|
||||||
|
"error_permission_denied": "Camera permission is required to scan QR codes.",
|
||||||
|
"login_as": "Log in as {{username}}?",
|
||||||
|
"on_server": "on {{server}}",
|
||||||
|
"use_different_user": "Use a different user",
|
||||||
|
"open_settings": "Open Settings"
|
||||||
|
},
|
||||||
|
"pairing": {
|
||||||
|
"pair_with_phone": "Pair with Phone",
|
||||||
|
"pair_with_phone_title": "Login TV",
|
||||||
|
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
|
||||||
|
"waiting_for_phone": "Waiting for phone...",
|
||||||
|
"scan_with_phone": "Scan with the Streamyfin app on your phone",
|
||||||
|
"logging_in": "Logging in...",
|
||||||
|
"logging_in_description": "Connecting to your server"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
"error_title": "Error",
|
"error_title": "Error",
|
||||||
"login_title": "Inicia sessió",
|
"login_title": "Inicia sessió",
|
||||||
"login_to_title": "Inicia sessió a",
|
"login_to_title": "Inicia sessió a",
|
||||||
|
"select_user": "Select a user to log in",
|
||||||
|
"add_user_to_login": "Add a user to log in",
|
||||||
|
"add_user": "Add User",
|
||||||
"username_placeholder": "Nom d'usuari",
|
"username_placeholder": "Nom d'usuari",
|
||||||
"password_placeholder": "Contrasenya",
|
"password_placeholder": "Contrasenya",
|
||||||
"login_button": "Inicia sessió",
|
"login_button": "Inicia sessió",
|
||||||
@@ -42,7 +45,13 @@
|
|||||||
"accounts_count": "{{count}} accounts",
|
"accounts_count": "{{count}} accounts",
|
||||||
"select_account": "Select Account",
|
"select_account": "Select Account",
|
||||||
"add_account": "Add Account",
|
"add_account": "Add Account",
|
||||||
"remove_account_description": "This will remove the saved credentials for {{username}}."
|
"remove_account_description": "This will remove the saved credentials for {{username}}.",
|
||||||
|
"remove_server": "Remove Server",
|
||||||
|
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
|
||||||
|
"select_your_server": "Select Your Server",
|
||||||
|
"add_server_to_get_started": "Add a server to get started",
|
||||||
|
"add_server": "Add Server",
|
||||||
|
"change_server": "Change Server"
|
||||||
},
|
},
|
||||||
"save_account": {
|
"save_account": {
|
||||||
"title": "Save Account",
|
"title": "Save Account",
|
||||||
@@ -86,6 +95,7 @@
|
|||||||
"oops": "Oops!",
|
"oops": "Oops!",
|
||||||
"error_message": "Alguna cosa ha anat malament.\nTanqueu la sessió i torneu-la a iniciar.",
|
"error_message": "Alguna cosa ha anat malament.\nTanqueu la sessió i torneu-la a iniciar.",
|
||||||
"continue_watching": "Continua veient",
|
"continue_watching": "Continua veient",
|
||||||
|
"continue": "Continue",
|
||||||
"next_up": "A continuació",
|
"next_up": "A continuació",
|
||||||
"continue_and_next_up": "Continue & Next Up",
|
"continue_and_next_up": "Continue & Next Up",
|
||||||
"recently_added_in": "Afegit recentment a {{libraryName}}",
|
"recently_added_in": "Afegit recentment a {{libraryName}}",
|
||||||
@@ -109,6 +119,12 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"settings_title": "Configuració",
|
"settings_title": "Configuració",
|
||||||
"log_out_button": "Tanca sessió",
|
"log_out_button": "Tanca sessió",
|
||||||
|
"switch_user": {
|
||||||
|
"title": "Switch User",
|
||||||
|
"account": "Account",
|
||||||
|
"switch_user": "Switch User on This Server",
|
||||||
|
"current": "current"
|
||||||
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "Categories"
|
"title": "Categories"
|
||||||
},
|
},
|
||||||
@@ -121,7 +137,16 @@
|
|||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "Appearance",
|
"title": "Appearance",
|
||||||
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
||||||
"hide_remote_session_button": "Hide Remote Session Button"
|
"hide_remote_session_button": "Hide Remote Session Button",
|
||||||
|
"show_home_backdrop": "Dynamic Home Backdrop",
|
||||||
|
"show_hero_carousel": "Hero Carousel",
|
||||||
|
"show_series_poster_on_episode": "Show Series Poster on Episodes",
|
||||||
|
"theme_music": "Theme Music",
|
||||||
|
"display_size": "Display Size",
|
||||||
|
"display_size_small": "Small",
|
||||||
|
"display_size_default": "Default",
|
||||||
|
"display_size_large": "Large",
|
||||||
|
"display_size_extra_large": "Extra Large"
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"title": "Network",
|
"title": "Network",
|
||||||
@@ -174,6 +199,22 @@
|
|||||||
"rewind_length": "Durada del rebobinat",
|
"rewind_length": "Durada del rebobinat",
|
||||||
"seconds_unit": "s"
|
"seconds_unit": "s"
|
||||||
},
|
},
|
||||||
|
"buffer": {
|
||||||
|
"title": "Buffer Settings",
|
||||||
|
"cache_mode": "Cache Mode",
|
||||||
|
"cache_auto": "Auto",
|
||||||
|
"cache_yes": "Enabled",
|
||||||
|
"cache_no": "Disabled",
|
||||||
|
"buffer_duration": "Buffer Duration",
|
||||||
|
"max_cache_size": "Max Cache Size",
|
||||||
|
"max_backward_cache": "Max Backward Cache"
|
||||||
|
},
|
||||||
|
"vo_driver": {
|
||||||
|
"title": "Video Output",
|
||||||
|
"vo_mode": "VO Driver",
|
||||||
|
"gpu_next": "gpu-next (Recommended)",
|
||||||
|
"gpu": "gpu"
|
||||||
|
},
|
||||||
"gesture_controls": {
|
"gesture_controls": {
|
||||||
"gesture_controls_title": "Gesture Controls",
|
"gesture_controls_title": "Gesture Controls",
|
||||||
"horizontal_swipe_skip": "Horizontal Swipe to Skip",
|
"horizontal_swipe_skip": "Horizontal Swipe to Skip",
|
||||||
@@ -256,7 +297,23 @@
|
|||||||
"subtitle_font": "Subtitle Font",
|
"subtitle_font": "Subtitle Font",
|
||||||
"ksplayer_title": "KSPlayer Settings",
|
"ksplayer_title": "KSPlayer Settings",
|
||||||
"hardware_decode": "Hardware Decoding",
|
"hardware_decode": "Hardware Decoding",
|
||||||
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
|
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.",
|
||||||
|
"opensubtitles_title": "OpenSubtitles",
|
||||||
|
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
|
||||||
|
"opensubtitles_api_key": "API Key",
|
||||||
|
"opensubtitles_api_key_placeholder": "Enter API key...",
|
||||||
|
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
|
||||||
|
"mpv_subtitle_scale": "Subtitle Scale",
|
||||||
|
"mpv_subtitle_margin_y": "Vertical Margin",
|
||||||
|
"mpv_subtitle_align_x": "Horizontal Align",
|
||||||
|
"mpv_subtitle_align_y": "Vertical Align",
|
||||||
|
"align": {
|
||||||
|
"left": "Left",
|
||||||
|
"center": "Center",
|
||||||
|
"right": "Right",
|
||||||
|
"top": "Top",
|
||||||
|
"bottom": "Bottom"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"vlc_subtitles": {
|
"vlc_subtitles": {
|
||||||
"title": "VLC Subtitle Settings",
|
"title": "VLC Subtitle Settings",
|
||||||
@@ -406,7 +463,13 @@
|
|||||||
"music_cache_cleared": "Music cache cleared",
|
"music_cache_cleared": "Music cache cleared",
|
||||||
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
||||||
"downloaded_songs_size": "{{size}} downloaded",
|
"downloaded_songs_size": "{{size}} downloaded",
|
||||||
"downloaded_songs_deleted": "Downloaded songs deleted"
|
"downloaded_songs_deleted": "Downloaded songs deleted",
|
||||||
|
"clear_all_cache": "Clear All Cache",
|
||||||
|
"clear_all_cache_confirm": "Clear All Cache?",
|
||||||
|
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
|
||||||
|
"clear_all_cache_success": "Cache Cleared",
|
||||||
|
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
|
||||||
|
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "Intro",
|
"title": "Intro",
|
||||||
@@ -430,6 +493,21 @@
|
|||||||
"error_deleting_files": "Error en suprimir fitxers",
|
"error_deleting_files": "Error en suprimir fitxers",
|
||||||
"background_downloads_enabled": "Descàrregues en segon pla activades",
|
"background_downloads_enabled": "Descàrregues en segon pla activades",
|
||||||
"background_downloads_disabled": "Descàrregues en segon pla desactivades"
|
"background_downloads_disabled": "Descàrregues en segon pla desactivades"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"title": "Security",
|
||||||
|
"inactivity_timeout": {
|
||||||
|
"title": "Inactivity Timeout",
|
||||||
|
"description": "Auto logout after inactivity",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"1_minute": "1 minute",
|
||||||
|
"5_minutes": "5 minutes",
|
||||||
|
"15_minutes": "15 minutes",
|
||||||
|
"30_minutes": "30 minutes",
|
||||||
|
"1_hour": "1 hour",
|
||||||
|
"4_hours": "4 hours",
|
||||||
|
"24_hours": "24 hours"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -456,6 +534,7 @@
|
|||||||
"new_app_version_requires_re_download_description": "L'actualització nova requereix que el contingut es torni a descarregar. Suprimiu tot el contingut descarregat i torneu-ho a provar.",
|
"new_app_version_requires_re_download_description": "L'actualització nova requereix que el contingut es torni a descarregar. Suprimiu tot el contingut descarregat i torneu-ho a provar.",
|
||||||
"back": "Enrere",
|
"back": "Enrere",
|
||||||
"delete": "Suprimeix",
|
"delete": "Suprimeix",
|
||||||
|
"delete_download": "Delete Download",
|
||||||
"something_went_wrong": "Alguna cosa ha anat malament",
|
"something_went_wrong": "Alguna cosa ha anat malament",
|
||||||
"could_not_get_stream_url_from_jellyfin": "No s'ha pogut obtenir l'URL del flux de Jellyfin",
|
"could_not_get_stream_url_from_jellyfin": "No s'ha pogut obtenir l'URL del flux de Jellyfin",
|
||||||
"eta": "ETA {{eta}}",
|
"eta": "ETA {{eta}}",
|
||||||
@@ -492,22 +571,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"no_results": "No Results",
|
||||||
"select": "Select",
|
"select": "Select",
|
||||||
"no_trailer_available": "No trailer available",
|
"no_trailer_available": "No trailer available",
|
||||||
"video": "Vídeo",
|
"video": "Vídeo",
|
||||||
"audio": "Àudio",
|
"audio": "Àudio",
|
||||||
"subtitle": "Subtítols",
|
"subtitle": "Subtítols",
|
||||||
"play": "Play",
|
"play": "Play",
|
||||||
|
"mark_as_played": "Mark as Played",
|
||||||
|
"mark_as_not_played": "Mark as not Played",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
"track": "Track",
|
"track": "Track",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
|
"stop": "Stop",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"verifying": "Verifying..."
|
"verifying": "Verifying...",
|
||||||
|
"login": "Login",
|
||||||
|
"refresh": "Refresh"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"search": "Cerca...",
|
"search": "Cerca...",
|
||||||
@@ -556,6 +641,7 @@
|
|||||||
"movies": "pel·lícules",
|
"movies": "pel·lícules",
|
||||||
"series": "sèries",
|
"series": "sèries",
|
||||||
"boxsets": "col·leccions",
|
"boxsets": "col·leccions",
|
||||||
|
"playlists": "Playlists",
|
||||||
"items": "elements"
|
"items": "elements"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
@@ -566,7 +652,8 @@
|
|||||||
"poster": "Cartell",
|
"poster": "Cartell",
|
||||||
"cover": "Coberta",
|
"cover": "Coberta",
|
||||||
"show_titles": "Mostrar títols",
|
"show_titles": "Mostrar títols",
|
||||||
"show_stats": "Mostrar estadístiques"
|
"show_stats": "Mostrar estadístiques",
|
||||||
|
"options_title": "Options"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"genres": "Gèneres",
|
"genres": "Gèneres",
|
||||||
@@ -574,7 +661,11 @@
|
|||||||
"sort_by": "Ordenar per",
|
"sort_by": "Ordenar per",
|
||||||
"filter_by": "Filter By",
|
"filter_by": "Filter By",
|
||||||
"sort_order": "Ordre",
|
"sort_order": "Ordre",
|
||||||
"tags": "Etiquetes"
|
"tags": "Etiquetes",
|
||||||
|
"all": "All",
|
||||||
|
"reset": "Reset",
|
||||||
|
"asc": "Ascending",
|
||||||
|
"desc": "Descending"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"favorites": {
|
"favorites": {
|
||||||
@@ -591,6 +682,8 @@
|
|||||||
"no_links": "No hi ha enllaços"
|
"no_links": "No hi ha enllaços"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
|
"live": "LIVE",
|
||||||
|
"mpv_player_title": "MPV Player",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"failed_to_get_stream_url": "No s'ha pogut obtenir l'URL del flux",
|
"failed_to_get_stream_url": "No s'ha pogut obtenir l'URL del flux",
|
||||||
"an_error_occured_while_playing_the_video": "S'ha produït un error en reproduir el vídeo. Consulteu els registres a la configuració.",
|
"an_error_occured_while_playing_the_video": "S'ha produït un error en reproduir el vídeo. Consulteu els registres a la configuració.",
|
||||||
@@ -608,7 +701,35 @@
|
|||||||
"downloaded_file_message": "Do you want to play the downloaded file?",
|
"downloaded_file_message": "Do you want to play the downloaded file?",
|
||||||
"downloaded_file_yes": "Yes",
|
"downloaded_file_yes": "Yes",
|
||||||
"downloaded_file_no": "No",
|
"downloaded_file_no": "No",
|
||||||
"downloaded_file_cancel": "Cancel"
|
"downloaded_file_cancel": "Cancel",
|
||||||
|
"swipe_down_settings": "Swipe down for settings",
|
||||||
|
"ends_at": "Ends at {{time}}",
|
||||||
|
"search_subtitles": "Search Subtitles",
|
||||||
|
"subtitle_tracks": "Tracks",
|
||||||
|
"subtitle_search": "Search & Download",
|
||||||
|
"download": "Download",
|
||||||
|
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
|
||||||
|
"using_jellyfin_server": "Using Jellyfin Server",
|
||||||
|
"language": "Language",
|
||||||
|
"results": "Results",
|
||||||
|
"searching": "Searching...",
|
||||||
|
"search_failed": "Search failed",
|
||||||
|
"no_subtitle_provider": "No subtitle provider configured on server",
|
||||||
|
"no_subtitles_found": "No subtitles found",
|
||||||
|
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
|
||||||
|
"settings": "Settings",
|
||||||
|
"skip_intro": "Skip Intro",
|
||||||
|
"skip_credits": "Skip Credits",
|
||||||
|
"stopPlayback": "Stop Playback",
|
||||||
|
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
|
||||||
|
"stopPlayingConfirm": "Are you sure you want to stop playback?",
|
||||||
|
"downloaded": "Downloaded"
|
||||||
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "A continuació",
|
"next_up": "A continuació",
|
||||||
@@ -617,6 +738,11 @@
|
|||||||
"series": "Sèries",
|
"series": "Sèries",
|
||||||
"seasons": "Temporades",
|
"seasons": "Temporades",
|
||||||
"season": "Temporada",
|
"season": "Temporada",
|
||||||
|
"from_this_series": "From This Series",
|
||||||
|
"more_from_this_season": "More from this Season",
|
||||||
|
"view_series": "View Series",
|
||||||
|
"view_season": "View Season",
|
||||||
|
"select_season": "Select Season",
|
||||||
"no_episodes_for_this_season": "No hi ha episodis per a aquesta temporada",
|
"no_episodes_for_this_season": "No hi ha episodis per a aquesta temporada",
|
||||||
"overview": "Descripció general",
|
"overview": "Descripció general",
|
||||||
"more_with": "Més amb {{name}}",
|
"more_with": "Més amb {{name}}",
|
||||||
@@ -627,10 +753,21 @@
|
|||||||
"media_options": "Media Options",
|
"media_options": "Media Options",
|
||||||
"quality": "Qualitat",
|
"quality": "Qualitat",
|
||||||
"audio": "Àudio",
|
"audio": "Àudio",
|
||||||
"subtitles": "Subtítols",
|
"subtitles": {
|
||||||
|
"label": "Subtitle",
|
||||||
|
"none": "None",
|
||||||
|
"tracks": "Tracks"
|
||||||
|
},
|
||||||
"show_more": "Mostra més",
|
"show_more": "Mostra més",
|
||||||
"show_less": "Mostra menys",
|
"show_less": "Mostra menys",
|
||||||
|
"left": "left",
|
||||||
|
"more_info": "More Info",
|
||||||
|
"director": "Director",
|
||||||
|
"cast": "Cast",
|
||||||
|
"technical_details": "Technical Details",
|
||||||
"appeared_in": "Va aparèixer a",
|
"appeared_in": "Va aparèixer a",
|
||||||
|
"movies": "Movies",
|
||||||
|
"shows": "Shows",
|
||||||
"could_not_load_item": "No s'ha pogut carregar l'element",
|
"could_not_load_item": "No s'ha pogut carregar l'element",
|
||||||
"none": "Cap",
|
"none": "Cap",
|
||||||
"download": {
|
"download": {
|
||||||
@@ -641,7 +778,13 @@
|
|||||||
"download_x_item": "Descarrega {{item_count}} elements",
|
"download_x_item": "Descarrega {{item_count}} elements",
|
||||||
"download_unwatched_only": "Unwatched Only",
|
"download_unwatched_only": "Unwatched Only",
|
||||||
"download_button": "Descarrega"
|
"download_button": "Descarrega"
|
||||||
}
|
},
|
||||||
|
"mark_played": "Mark as Watched",
|
||||||
|
"mark_unplayed": "Mark as Unwatched",
|
||||||
|
"resume_playback": "Resume Playback",
|
||||||
|
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
|
||||||
|
"play_from_start": "Play from Start",
|
||||||
|
"continue_from": "Continue from {{time}}"
|
||||||
},
|
},
|
||||||
"live_tv": {
|
"live_tv": {
|
||||||
"next": "Següent",
|
"next": "Següent",
|
||||||
@@ -652,7 +795,18 @@
|
|||||||
"movies": "Pel·lícules",
|
"movies": "Pel·lícules",
|
||||||
"sports": "Esports",
|
"sports": "Esports",
|
||||||
"for_kids": "Infantil",
|
"for_kids": "Infantil",
|
||||||
"news": "Notícies"
|
"news": "Notícies",
|
||||||
|
"page_of": "Page {{current}} of {{total}}",
|
||||||
|
"no_programs": "No programs available",
|
||||||
|
"no_channels": "No channels available",
|
||||||
|
"tabs": {
|
||||||
|
"programs": "Programs",
|
||||||
|
"guide": "Guide",
|
||||||
|
"channels": "Channels",
|
||||||
|
"recordings": "Recordings",
|
||||||
|
"schedule": "Schedule",
|
||||||
|
"series": "Series"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"jellyseerr": {
|
"jellyseerr": {
|
||||||
"confirm": "Confirma",
|
"confirm": "Confirma",
|
||||||
@@ -697,6 +851,12 @@
|
|||||||
"decline": "Decline",
|
"decline": "Decline",
|
||||||
"requested_by": "Requested by {{user}}",
|
"requested_by": "Requested by {{user}}",
|
||||||
"unknown_user": "Unknown User",
|
"unknown_user": "Unknown User",
|
||||||
|
"select": "Select",
|
||||||
|
"request_all": "Request All",
|
||||||
|
"request_seasons": "Request Seasons",
|
||||||
|
"select_seasons": "Select Seasons",
|
||||||
|
"request_selected": "Request Selected",
|
||||||
|
"n_selected": "{{count}} selected",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"jellyseer_does_not_meet_requirements": "El servidor Jellyseerr no compleix els requisits mínims de versió! Actualitzeu-lo almenys a la versió 2.0.0",
|
"jellyseer_does_not_meet_requirements": "El servidor Jellyseerr no compleix els requisits mínims de versió! Actualitzeu-lo almenys a la versió 2.0.0",
|
||||||
"jellyseerr_test_failed": "Ha fallat la prova de Jellyseerr. Torneu-ho a provar.",
|
"jellyseerr_test_failed": "Ha fallat la prova de Jellyseerr. Torneu-ho a provar.",
|
||||||
@@ -716,7 +876,8 @@
|
|||||||
"search": "Cercar",
|
"search": "Cercar",
|
||||||
"library": "Biblioteca",
|
"library": "Biblioteca",
|
||||||
"custom_links": "Enllaços personalitzats",
|
"custom_links": "Enllaços personalitzats",
|
||||||
"favorites": "Preferits"
|
"favorites": "Preferits",
|
||||||
|
"settings": "Settings"
|
||||||
},
|
},
|
||||||
"music": {
|
"music": {
|
||||||
"title": "Music",
|
"title": "Music",
|
||||||
@@ -841,5 +1002,36 @@
|
|||||||
"show": "This show",
|
"show": "This show",
|
||||||
"all": "All media (default)"
|
"all": "All media (default)"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"companion_login": {
|
||||||
|
"title": "Pair with TV",
|
||||||
|
"align_qr": "Align the QR code within the frame",
|
||||||
|
"enter_code_manually": "Enter code manually",
|
||||||
|
"pairing_enter_credentials": "Enter credentials for TV",
|
||||||
|
"pairing_code_label": "Pairing code",
|
||||||
|
"server": "Server",
|
||||||
|
"authorize_button": "Authorize",
|
||||||
|
"authorizing": "Authorizing...",
|
||||||
|
"scan_again": "Scan Again",
|
||||||
|
"done": "Done",
|
||||||
|
"success_title": "Authorization Sent",
|
||||||
|
"pairing_tv_connecting": "The TV is connecting to your account",
|
||||||
|
"error_title": "Authorization Failed",
|
||||||
|
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
|
||||||
|
"error_generic": "Something went wrong. Please try again.",
|
||||||
|
"error_permission_denied": "Camera permission is required to scan QR codes.",
|
||||||
|
"login_as": "Log in as {{username}}?",
|
||||||
|
"on_server": "on {{server}}",
|
||||||
|
"use_different_user": "Use a different user",
|
||||||
|
"open_settings": "Open Settings"
|
||||||
|
},
|
||||||
|
"pairing": {
|
||||||
|
"pair_with_phone": "Pair with Phone",
|
||||||
|
"pair_with_phone_title": "Login TV",
|
||||||
|
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
|
||||||
|
"waiting_for_phone": "Waiting for phone...",
|
||||||
|
"scan_with_phone": "Scan with the Streamyfin app on your phone",
|
||||||
|
"logging_in": "Logging in...",
|
||||||
|
"logging_in_description": "Connecting to your server"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
"error_title": "Chyba",
|
"error_title": "Chyba",
|
||||||
"login_title": "Přihlásit se",
|
"login_title": "Přihlásit se",
|
||||||
"login_to_title": "Přihlásit se do",
|
"login_to_title": "Přihlásit se do",
|
||||||
|
"select_user": "Select a user to log in",
|
||||||
|
"add_user_to_login": "Add a user to log in",
|
||||||
|
"add_user": "Add User",
|
||||||
"username_placeholder": "Uživatelské jméno",
|
"username_placeholder": "Uživatelské jméno",
|
||||||
"password_placeholder": "Heslo",
|
"password_placeholder": "Heslo",
|
||||||
"login_button": "Přihlásit se",
|
"login_button": "Přihlásit se",
|
||||||
@@ -42,7 +45,13 @@
|
|||||||
"accounts_count": "{{count}} accounts",
|
"accounts_count": "{{count}} accounts",
|
||||||
"select_account": "Select Account",
|
"select_account": "Select Account",
|
||||||
"add_account": "Add Account",
|
"add_account": "Add Account",
|
||||||
"remove_account_description": "This will remove the saved credentials for {{username}}."
|
"remove_account_description": "This will remove the saved credentials for {{username}}.",
|
||||||
|
"remove_server": "Remove Server",
|
||||||
|
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
|
||||||
|
"select_your_server": "Select Your Server",
|
||||||
|
"add_server_to_get_started": "Add a server to get started",
|
||||||
|
"add_server": "Add Server",
|
||||||
|
"change_server": "Change Server"
|
||||||
},
|
},
|
||||||
"save_account": {
|
"save_account": {
|
||||||
"title": "Save Account",
|
"title": "Save Account",
|
||||||
@@ -86,6 +95,7 @@
|
|||||||
"oops": "Jejda!",
|
"oops": "Jejda!",
|
||||||
"error_message": "Něco se pokazilo.\nOdhlaste se a znovu se prosím.",
|
"error_message": "Něco se pokazilo.\nOdhlaste se a znovu se prosím.",
|
||||||
"continue_watching": "Pokračovat ve sledování",
|
"continue_watching": "Pokračovat ve sledování",
|
||||||
|
"continue": "Continue",
|
||||||
"next_up": "Další nahoru",
|
"next_up": "Další nahoru",
|
||||||
"continue_and_next_up": "Continue & Next Up",
|
"continue_and_next_up": "Continue & Next Up",
|
||||||
"recently_added_in": "Nedávno přidané v {{libraryName}}",
|
"recently_added_in": "Nedávno přidané v {{libraryName}}",
|
||||||
@@ -109,6 +119,12 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"settings_title": "Nastavení",
|
"settings_title": "Nastavení",
|
||||||
"log_out_button": "Odhlásit se",
|
"log_out_button": "Odhlásit se",
|
||||||
|
"switch_user": {
|
||||||
|
"title": "Switch User",
|
||||||
|
"account": "Account",
|
||||||
|
"switch_user": "Switch User on This Server",
|
||||||
|
"current": "current"
|
||||||
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "Categories"
|
"title": "Categories"
|
||||||
},
|
},
|
||||||
@@ -121,7 +137,16 @@
|
|||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "Appearance",
|
"title": "Appearance",
|
||||||
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
||||||
"hide_remote_session_button": "Hide Remote Session Button"
|
"hide_remote_session_button": "Hide Remote Session Button",
|
||||||
|
"show_home_backdrop": "Dynamic Home Backdrop",
|
||||||
|
"show_hero_carousel": "Hero Carousel",
|
||||||
|
"show_series_poster_on_episode": "Show Series Poster on Episodes",
|
||||||
|
"theme_music": "Theme Music",
|
||||||
|
"display_size": "Display Size",
|
||||||
|
"display_size_small": "Small",
|
||||||
|
"display_size_default": "Default",
|
||||||
|
"display_size_large": "Large",
|
||||||
|
"display_size_extra_large": "Extra Large"
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"title": "Network",
|
"title": "Network",
|
||||||
@@ -174,6 +199,22 @@
|
|||||||
"rewind_length": "Délka zpětného větru",
|
"rewind_length": "Délka zpětného větru",
|
||||||
"seconds_unit": "s"
|
"seconds_unit": "s"
|
||||||
},
|
},
|
||||||
|
"buffer": {
|
||||||
|
"title": "Buffer Settings",
|
||||||
|
"cache_mode": "Cache Mode",
|
||||||
|
"cache_auto": "Auto",
|
||||||
|
"cache_yes": "Enabled",
|
||||||
|
"cache_no": "Disabled",
|
||||||
|
"buffer_duration": "Buffer Duration",
|
||||||
|
"max_cache_size": "Max Cache Size",
|
||||||
|
"max_backward_cache": "Max Backward Cache"
|
||||||
|
},
|
||||||
|
"vo_driver": {
|
||||||
|
"title": "Video Output",
|
||||||
|
"vo_mode": "VO Driver",
|
||||||
|
"gpu_next": "gpu-next (Recommended)",
|
||||||
|
"gpu": "gpu"
|
||||||
|
},
|
||||||
"gesture_controls": {
|
"gesture_controls": {
|
||||||
"gesture_controls_title": "Ovládání gest",
|
"gesture_controls_title": "Ovládání gest",
|
||||||
"horizontal_swipe_skip": "Horizontální přejetím přeskočit",
|
"horizontal_swipe_skip": "Horizontální přejetím přeskočit",
|
||||||
@@ -256,7 +297,23 @@
|
|||||||
"subtitle_font": "Subtitle Font",
|
"subtitle_font": "Subtitle Font",
|
||||||
"ksplayer_title": "KSPlayer Settings",
|
"ksplayer_title": "KSPlayer Settings",
|
||||||
"hardware_decode": "Hardware Decoding",
|
"hardware_decode": "Hardware Decoding",
|
||||||
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
|
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.",
|
||||||
|
"opensubtitles_title": "OpenSubtitles",
|
||||||
|
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
|
||||||
|
"opensubtitles_api_key": "API Key",
|
||||||
|
"opensubtitles_api_key_placeholder": "Enter API key...",
|
||||||
|
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
|
||||||
|
"mpv_subtitle_scale": "Subtitle Scale",
|
||||||
|
"mpv_subtitle_margin_y": "Vertical Margin",
|
||||||
|
"mpv_subtitle_align_x": "Horizontal Align",
|
||||||
|
"mpv_subtitle_align_y": "Vertical Align",
|
||||||
|
"align": {
|
||||||
|
"left": "Left",
|
||||||
|
"center": "Center",
|
||||||
|
"right": "Right",
|
||||||
|
"top": "Top",
|
||||||
|
"bottom": "Bottom"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"vlc_subtitles": {
|
"vlc_subtitles": {
|
||||||
"title": "VLC Subtitle Settings",
|
"title": "VLC Subtitle Settings",
|
||||||
@@ -406,7 +463,13 @@
|
|||||||
"music_cache_cleared": "Music cache cleared",
|
"music_cache_cleared": "Music cache cleared",
|
||||||
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
||||||
"downloaded_songs_size": "{{size}} downloaded",
|
"downloaded_songs_size": "{{size}} downloaded",
|
||||||
"downloaded_songs_deleted": "Downloaded songs deleted"
|
"downloaded_songs_deleted": "Downloaded songs deleted",
|
||||||
|
"clear_all_cache": "Clear All Cache",
|
||||||
|
"clear_all_cache_confirm": "Clear All Cache?",
|
||||||
|
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
|
||||||
|
"clear_all_cache_success": "Cache Cleared",
|
||||||
|
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
|
||||||
|
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "Intro",
|
"title": "Intro",
|
||||||
@@ -430,6 +493,21 @@
|
|||||||
"error_deleting_files": "Chyba při mazání souborů",
|
"error_deleting_files": "Chyba při mazání souborů",
|
||||||
"background_downloads_enabled": "Stahování na pozadí povoleno",
|
"background_downloads_enabled": "Stahování na pozadí povoleno",
|
||||||
"background_downloads_disabled": "Stahování na pozadí zakázáno"
|
"background_downloads_disabled": "Stahování na pozadí zakázáno"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"title": "Security",
|
||||||
|
"inactivity_timeout": {
|
||||||
|
"title": "Inactivity Timeout",
|
||||||
|
"description": "Auto logout after inactivity",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"1_minute": "1 minute",
|
||||||
|
"5_minutes": "5 minutes",
|
||||||
|
"15_minutes": "15 minutes",
|
||||||
|
"30_minutes": "30 minutes",
|
||||||
|
"1_hour": "1 hour",
|
||||||
|
"4_hours": "4 hours",
|
||||||
|
"24_hours": "24 hours"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -456,6 +534,7 @@
|
|||||||
"new_app_version_requires_re_download_description": "Nová aktualizace vyžaduje opětovné stažení obsahu. Odstraňte prosím veškerý stažený obsah a zkuste to znovu.",
|
"new_app_version_requires_re_download_description": "Nová aktualizace vyžaduje opětovné stažení obsahu. Odstraňte prosím veškerý stažený obsah a zkuste to znovu.",
|
||||||
"back": "Zpět",
|
"back": "Zpět",
|
||||||
"delete": "Vymazat",
|
"delete": "Vymazat",
|
||||||
|
"delete_download": "Delete Download",
|
||||||
"something_went_wrong": "Něco se pokazilo",
|
"something_went_wrong": "Něco se pokazilo",
|
||||||
"could_not_get_stream_url_from_jellyfin": "Nelze získat URL streamu z Jellyfin",
|
"could_not_get_stream_url_from_jellyfin": "Nelze získat URL streamu z Jellyfin",
|
||||||
"eta": "ETA {{eta}}",
|
"eta": "ETA {{eta}}",
|
||||||
@@ -492,22 +571,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"no_results": "No Results",
|
||||||
"select": "Vybrat",
|
"select": "Vybrat",
|
||||||
"no_trailer_available": "Přípojné vozidlo není k dispozici",
|
"no_trailer_available": "Přípojné vozidlo není k dispozici",
|
||||||
"video": "Video",
|
"video": "Video",
|
||||||
"audio": "Zvuk",
|
"audio": "Zvuk",
|
||||||
"subtitle": "Podtitulek",
|
"subtitle": "Podtitulek",
|
||||||
"play": "Hrát",
|
"play": "Hrát",
|
||||||
|
"mark_as_played": "Mark as Played",
|
||||||
|
"mark_as_not_played": "Mark as not Played",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
"track": "Track",
|
"track": "Track",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
|
"stop": "Stop",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"verifying": "Verifying..."
|
"verifying": "Verifying...",
|
||||||
|
"login": "Login",
|
||||||
|
"refresh": "Refresh"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"search": "Hledat...",
|
"search": "Hledat...",
|
||||||
@@ -556,6 +641,7 @@
|
|||||||
"movies": "Filmy",
|
"movies": "Filmy",
|
||||||
"series": "Série",
|
"series": "Série",
|
||||||
"boxsets": "Sada boxů",
|
"boxsets": "Sada boxů",
|
||||||
|
"playlists": "Playlists",
|
||||||
"items": "Položky"
|
"items": "Položky"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
@@ -566,7 +652,8 @@
|
|||||||
"poster": "Plakát",
|
"poster": "Plakát",
|
||||||
"cover": "Kryt",
|
"cover": "Kryt",
|
||||||
"show_titles": "Zobrazit názvy",
|
"show_titles": "Zobrazit názvy",
|
||||||
"show_stats": "Zobrazit statistiky"
|
"show_stats": "Zobrazit statistiky",
|
||||||
|
"options_title": "Options"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"genres": "Genres",
|
"genres": "Genres",
|
||||||
@@ -574,7 +661,11 @@
|
|||||||
"sort_by": "Seřadit podle",
|
"sort_by": "Seřadit podle",
|
||||||
"filter_by": "Filter By",
|
"filter_by": "Filter By",
|
||||||
"sort_order": "Řazení",
|
"sort_order": "Řazení",
|
||||||
"tags": "Štítky"
|
"tags": "Štítky",
|
||||||
|
"all": "All",
|
||||||
|
"reset": "Reset",
|
||||||
|
"asc": "Ascending",
|
||||||
|
"desc": "Descending"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"favorites": {
|
"favorites": {
|
||||||
@@ -591,6 +682,8 @@
|
|||||||
"no_links": "Žádné odkazy"
|
"no_links": "Žádné odkazy"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
|
"live": "LIVE",
|
||||||
|
"mpv_player_title": "MPV Player",
|
||||||
"error": "Chyba",
|
"error": "Chyba",
|
||||||
"failed_to_get_stream_url": "Nepodařilo se získat URL streamu",
|
"failed_to_get_stream_url": "Nepodařilo se získat URL streamu",
|
||||||
"an_error_occured_while_playing_the_video": "Při přehrávání videa došlo k chybě. Zkontrolujte logy v nastavení.",
|
"an_error_occured_while_playing_the_video": "Při přehrávání videa došlo k chybě. Zkontrolujte logy v nastavení.",
|
||||||
@@ -608,7 +701,35 @@
|
|||||||
"downloaded_file_message": "Do you want to play the downloaded file?",
|
"downloaded_file_message": "Do you want to play the downloaded file?",
|
||||||
"downloaded_file_yes": "Yes",
|
"downloaded_file_yes": "Yes",
|
||||||
"downloaded_file_no": "No",
|
"downloaded_file_no": "No",
|
||||||
"downloaded_file_cancel": "Cancel"
|
"downloaded_file_cancel": "Cancel",
|
||||||
|
"swipe_down_settings": "Swipe down for settings",
|
||||||
|
"ends_at": "Ends at {{time}}",
|
||||||
|
"search_subtitles": "Search Subtitles",
|
||||||
|
"subtitle_tracks": "Tracks",
|
||||||
|
"subtitle_search": "Search & Download",
|
||||||
|
"download": "Download",
|
||||||
|
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
|
||||||
|
"using_jellyfin_server": "Using Jellyfin Server",
|
||||||
|
"language": "Language",
|
||||||
|
"results": "Results",
|
||||||
|
"searching": "Searching...",
|
||||||
|
"search_failed": "Search failed",
|
||||||
|
"no_subtitle_provider": "No subtitle provider configured on server",
|
||||||
|
"no_subtitles_found": "No subtitles found",
|
||||||
|
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
|
||||||
|
"settings": "Settings",
|
||||||
|
"skip_intro": "Skip Intro",
|
||||||
|
"skip_credits": "Skip Credits",
|
||||||
|
"stopPlayback": "Stop Playback",
|
||||||
|
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
|
||||||
|
"stopPlayingConfirm": "Are you sure you want to stop playback?",
|
||||||
|
"downloaded": "Downloaded"
|
||||||
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Další nahoru",
|
"next_up": "Další nahoru",
|
||||||
@@ -617,6 +738,11 @@
|
|||||||
"series": "Série",
|
"series": "Série",
|
||||||
"seasons": "Série",
|
"seasons": "Série",
|
||||||
"season": "Sezóna",
|
"season": "Sezóna",
|
||||||
|
"from_this_series": "From This Series",
|
||||||
|
"more_from_this_season": "More from this Season",
|
||||||
|
"view_series": "View Series",
|
||||||
|
"view_season": "View Season",
|
||||||
|
"select_season": "Select Season",
|
||||||
"no_episodes_for_this_season": "Žádné epizody pro tuto sezónu",
|
"no_episodes_for_this_season": "Žádné epizody pro tuto sezónu",
|
||||||
"overview": "Přehled",
|
"overview": "Přehled",
|
||||||
"more_with": "Více s {{name}}",
|
"more_with": "Více s {{name}}",
|
||||||
@@ -627,10 +753,21 @@
|
|||||||
"media_options": "Media Options",
|
"media_options": "Media Options",
|
||||||
"quality": "Kvalita",
|
"quality": "Kvalita",
|
||||||
"audio": "Zvuk",
|
"audio": "Zvuk",
|
||||||
"subtitles": "Podtitulek",
|
"subtitles": {
|
||||||
|
"label": "Subtitle",
|
||||||
|
"none": "None",
|
||||||
|
"tracks": "Tracks"
|
||||||
|
},
|
||||||
"show_more": "Zobrazit více",
|
"show_more": "Zobrazit více",
|
||||||
"show_less": "Zobrazit méně",
|
"show_less": "Zobrazit méně",
|
||||||
|
"left": "left",
|
||||||
|
"more_info": "More Info",
|
||||||
|
"director": "Director",
|
||||||
|
"cast": "Cast",
|
||||||
|
"technical_details": "Technical Details",
|
||||||
"appeared_in": "Zobrazeno v",
|
"appeared_in": "Zobrazeno v",
|
||||||
|
"movies": "Movies",
|
||||||
|
"shows": "Shows",
|
||||||
"could_not_load_item": "Nelze načíst položku",
|
"could_not_load_item": "Nelze načíst položku",
|
||||||
"none": "Nic",
|
"none": "Nic",
|
||||||
"download": {
|
"download": {
|
||||||
@@ -641,7 +778,13 @@
|
|||||||
"download_x_item": "Stáhnout položky {{item_count}}",
|
"download_x_item": "Stáhnout položky {{item_count}}",
|
||||||
"download_unwatched_only": "Pouze nezhlédnuté",
|
"download_unwatched_only": "Pouze nezhlédnuté",
|
||||||
"download_button": "Stáhnout"
|
"download_button": "Stáhnout"
|
||||||
}
|
},
|
||||||
|
"mark_played": "Mark as Watched",
|
||||||
|
"mark_unplayed": "Mark as Unwatched",
|
||||||
|
"resume_playback": "Resume Playback",
|
||||||
|
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
|
||||||
|
"play_from_start": "Play from Start",
|
||||||
|
"continue_from": "Continue from {{time}}"
|
||||||
},
|
},
|
||||||
"live_tv": {
|
"live_tv": {
|
||||||
"next": "Další",
|
"next": "Další",
|
||||||
@@ -652,7 +795,18 @@
|
|||||||
"movies": "Filmy",
|
"movies": "Filmy",
|
||||||
"sports": "Sporty",
|
"sports": "Sporty",
|
||||||
"for_kids": "Pro děti",
|
"for_kids": "Pro děti",
|
||||||
"news": "Novinky"
|
"news": "Novinky",
|
||||||
|
"page_of": "Page {{current}} of {{total}}",
|
||||||
|
"no_programs": "No programs available",
|
||||||
|
"no_channels": "No channels available",
|
||||||
|
"tabs": {
|
||||||
|
"programs": "Programs",
|
||||||
|
"guide": "Guide",
|
||||||
|
"channels": "Channels",
|
||||||
|
"recordings": "Recordings",
|
||||||
|
"schedule": "Schedule",
|
||||||
|
"series": "Series"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"jellyseerr": {
|
"jellyseerr": {
|
||||||
"confirm": "Potvrdit",
|
"confirm": "Potvrdit",
|
||||||
@@ -697,6 +851,12 @@
|
|||||||
"decline": "Decline",
|
"decline": "Decline",
|
||||||
"requested_by": "Requested by {{user}}",
|
"requested_by": "Requested by {{user}}",
|
||||||
"unknown_user": "Unknown User",
|
"unknown_user": "Unknown User",
|
||||||
|
"select": "Select",
|
||||||
|
"request_all": "Request All",
|
||||||
|
"request_seasons": "Request Seasons",
|
||||||
|
"select_seasons": "Select Seasons",
|
||||||
|
"request_selected": "Request Selected",
|
||||||
|
"n_selected": "{{count}} selected",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"jellyseer_does_not_meet_requirements": "Seerr server nesplňuje minimální požadavky na verzi! Aktualizujte prosím alespoň na 2.0.0",
|
"jellyseer_does_not_meet_requirements": "Seerr server nesplňuje minimální požadavky na verzi! Aktualizujte prosím alespoň na 2.0.0",
|
||||||
"jellyseerr_test_failed": "Seerr test se nezdařil. Zkuste to prosím znovu.",
|
"jellyseerr_test_failed": "Seerr test se nezdařil. Zkuste to prosím znovu.",
|
||||||
@@ -716,7 +876,8 @@
|
|||||||
"search": "Hledat",
|
"search": "Hledat",
|
||||||
"library": "Knihovna",
|
"library": "Knihovna",
|
||||||
"custom_links": "Vlastní odkazy",
|
"custom_links": "Vlastní odkazy",
|
||||||
"favorites": "Oblíbené"
|
"favorites": "Oblíbené",
|
||||||
|
"settings": "Settings"
|
||||||
},
|
},
|
||||||
"music": {
|
"music": {
|
||||||
"title": "Music",
|
"title": "Music",
|
||||||
@@ -841,5 +1002,36 @@
|
|||||||
"show": "This show",
|
"show": "This show",
|
||||||
"all": "All media (default)"
|
"all": "All media (default)"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"companion_login": {
|
||||||
|
"title": "Pair with TV",
|
||||||
|
"align_qr": "Align the QR code within the frame",
|
||||||
|
"enter_code_manually": "Enter code manually",
|
||||||
|
"pairing_enter_credentials": "Enter credentials for TV",
|
||||||
|
"pairing_code_label": "Pairing code",
|
||||||
|
"server": "Server",
|
||||||
|
"authorize_button": "Authorize",
|
||||||
|
"authorizing": "Authorizing...",
|
||||||
|
"scan_again": "Scan Again",
|
||||||
|
"done": "Done",
|
||||||
|
"success_title": "Authorization Sent",
|
||||||
|
"pairing_tv_connecting": "The TV is connecting to your account",
|
||||||
|
"error_title": "Authorization Failed",
|
||||||
|
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
|
||||||
|
"error_generic": "Something went wrong. Please try again.",
|
||||||
|
"error_permission_denied": "Camera permission is required to scan QR codes.",
|
||||||
|
"login_as": "Log in as {{username}}?",
|
||||||
|
"on_server": "on {{server}}",
|
||||||
|
"use_different_user": "Use a different user",
|
||||||
|
"open_settings": "Open Settings"
|
||||||
|
},
|
||||||
|
"pairing": {
|
||||||
|
"pair_with_phone": "Pair with Phone",
|
||||||
|
"pair_with_phone_title": "Login TV",
|
||||||
|
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
|
||||||
|
"waiting_for_phone": "Waiting for phone...",
|
||||||
|
"scan_with_phone": "Scan with the Streamyfin app on your phone",
|
||||||
|
"logging_in": "Logging in...",
|
||||||
|
"logging_in_description": "Connecting to your server"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
"error_title": "Fejl",
|
"error_title": "Fejl",
|
||||||
"login_title": "Log ind",
|
"login_title": "Log ind",
|
||||||
"login_to_title": "Log ind på",
|
"login_to_title": "Log ind på",
|
||||||
|
"select_user": "Select a user to log in",
|
||||||
|
"add_user_to_login": "Add a user to log in",
|
||||||
|
"add_user": "Add User",
|
||||||
"username_placeholder": "Brugernavn",
|
"username_placeholder": "Brugernavn",
|
||||||
"password_placeholder": "Adgangskode",
|
"password_placeholder": "Adgangskode",
|
||||||
"login_button": "Log ind",
|
"login_button": "Log ind",
|
||||||
@@ -42,7 +45,13 @@
|
|||||||
"accounts_count": "{{count}} accounts",
|
"accounts_count": "{{count}} accounts",
|
||||||
"select_account": "Select Account",
|
"select_account": "Select Account",
|
||||||
"add_account": "Add Account",
|
"add_account": "Add Account",
|
||||||
"remove_account_description": "This will remove the saved credentials for {{username}}."
|
"remove_account_description": "This will remove the saved credentials for {{username}}.",
|
||||||
|
"remove_server": "Remove Server",
|
||||||
|
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
|
||||||
|
"select_your_server": "Select Your Server",
|
||||||
|
"add_server_to_get_started": "Add a server to get started",
|
||||||
|
"add_server": "Add Server",
|
||||||
|
"change_server": "Change Server"
|
||||||
},
|
},
|
||||||
"save_account": {
|
"save_account": {
|
||||||
"title": "Save Account",
|
"title": "Save Account",
|
||||||
@@ -86,6 +95,7 @@
|
|||||||
"oops": "Ups!",
|
"oops": "Ups!",
|
||||||
"error_message": "Noget gik galt.\nLog venligst ud og ind igen.",
|
"error_message": "Noget gik galt.\nLog venligst ud og ind igen.",
|
||||||
"continue_watching": "Fortsæt med at se",
|
"continue_watching": "Fortsæt med at se",
|
||||||
|
"continue": "Continue",
|
||||||
"next_up": "Næste",
|
"next_up": "Næste",
|
||||||
"continue_and_next_up": "Continue & Next Up",
|
"continue_and_next_up": "Continue & Next Up",
|
||||||
"recently_added_in": "Senest tilføjet i {{libraryName}}",
|
"recently_added_in": "Senest tilføjet i {{libraryName}}",
|
||||||
@@ -109,6 +119,12 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"settings_title": "Indstillinger",
|
"settings_title": "Indstillinger",
|
||||||
"log_out_button": "Log ud",
|
"log_out_button": "Log ud",
|
||||||
|
"switch_user": {
|
||||||
|
"title": "Switch User",
|
||||||
|
"account": "Account",
|
||||||
|
"switch_user": "Switch User on This Server",
|
||||||
|
"current": "current"
|
||||||
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "Categories"
|
"title": "Categories"
|
||||||
},
|
},
|
||||||
@@ -121,7 +137,16 @@
|
|||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "Appearance",
|
"title": "Appearance",
|
||||||
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
||||||
"hide_remote_session_button": "Hide Remote Session Button"
|
"hide_remote_session_button": "Hide Remote Session Button",
|
||||||
|
"show_home_backdrop": "Dynamic Home Backdrop",
|
||||||
|
"show_hero_carousel": "Hero Carousel",
|
||||||
|
"show_series_poster_on_episode": "Show Series Poster on Episodes",
|
||||||
|
"theme_music": "Theme Music",
|
||||||
|
"display_size": "Display Size",
|
||||||
|
"display_size_small": "Small",
|
||||||
|
"display_size_default": "Default",
|
||||||
|
"display_size_large": "Large",
|
||||||
|
"display_size_extra_large": "Extra Large"
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"title": "Network",
|
"title": "Network",
|
||||||
@@ -174,6 +199,22 @@
|
|||||||
"rewind_length": "Spol tilbage længde",
|
"rewind_length": "Spol tilbage længde",
|
||||||
"seconds_unit": "s"
|
"seconds_unit": "s"
|
||||||
},
|
},
|
||||||
|
"buffer": {
|
||||||
|
"title": "Buffer Settings",
|
||||||
|
"cache_mode": "Cache Mode",
|
||||||
|
"cache_auto": "Auto",
|
||||||
|
"cache_yes": "Enabled",
|
||||||
|
"cache_no": "Disabled",
|
||||||
|
"buffer_duration": "Buffer Duration",
|
||||||
|
"max_cache_size": "Max Cache Size",
|
||||||
|
"max_backward_cache": "Max Backward Cache"
|
||||||
|
},
|
||||||
|
"vo_driver": {
|
||||||
|
"title": "Video Output",
|
||||||
|
"vo_mode": "VO Driver",
|
||||||
|
"gpu_next": "gpu-next (Recommended)",
|
||||||
|
"gpu": "gpu"
|
||||||
|
},
|
||||||
"gesture_controls": {
|
"gesture_controls": {
|
||||||
"gesture_controls_title": "Bevægelsesstyring",
|
"gesture_controls_title": "Bevægelsesstyring",
|
||||||
"horizontal_swipe_skip": "Vandret Stryg for at springe over",
|
"horizontal_swipe_skip": "Vandret Stryg for at springe over",
|
||||||
@@ -256,7 +297,23 @@
|
|||||||
"subtitle_font": "Subtitle Font",
|
"subtitle_font": "Subtitle Font",
|
||||||
"ksplayer_title": "KSPlayer Settings",
|
"ksplayer_title": "KSPlayer Settings",
|
||||||
"hardware_decode": "Hardware Decoding",
|
"hardware_decode": "Hardware Decoding",
|
||||||
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
|
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.",
|
||||||
|
"opensubtitles_title": "OpenSubtitles",
|
||||||
|
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
|
||||||
|
"opensubtitles_api_key": "API Key",
|
||||||
|
"opensubtitles_api_key_placeholder": "Enter API key...",
|
||||||
|
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
|
||||||
|
"mpv_subtitle_scale": "Subtitle Scale",
|
||||||
|
"mpv_subtitle_margin_y": "Vertical Margin",
|
||||||
|
"mpv_subtitle_align_x": "Horizontal Align",
|
||||||
|
"mpv_subtitle_align_y": "Vertical Align",
|
||||||
|
"align": {
|
||||||
|
"left": "Left",
|
||||||
|
"center": "Center",
|
||||||
|
"right": "Right",
|
||||||
|
"top": "Top",
|
||||||
|
"bottom": "Bottom"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"vlc_subtitles": {
|
"vlc_subtitles": {
|
||||||
"title": "VLC Subtitle Settings",
|
"title": "VLC Subtitle Settings",
|
||||||
@@ -406,7 +463,13 @@
|
|||||||
"music_cache_cleared": "Music cache cleared",
|
"music_cache_cleared": "Music cache cleared",
|
||||||
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
||||||
"downloaded_songs_size": "{{size}} downloaded",
|
"downloaded_songs_size": "{{size}} downloaded",
|
||||||
"downloaded_songs_deleted": "Downloaded songs deleted"
|
"downloaded_songs_deleted": "Downloaded songs deleted",
|
||||||
|
"clear_all_cache": "Clear All Cache",
|
||||||
|
"clear_all_cache_confirm": "Clear All Cache?",
|
||||||
|
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
|
||||||
|
"clear_all_cache_success": "Cache Cleared",
|
||||||
|
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
|
||||||
|
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "Intro",
|
"title": "Intro",
|
||||||
@@ -430,6 +493,21 @@
|
|||||||
"error_deleting_files": "Fejl ved sletning af filer",
|
"error_deleting_files": "Fejl ved sletning af filer",
|
||||||
"background_downloads_enabled": "Baggrundsdownloads aktiveret",
|
"background_downloads_enabled": "Baggrundsdownloads aktiveret",
|
||||||
"background_downloads_disabled": "Baggrundsdownloads deaktiveret"
|
"background_downloads_disabled": "Baggrundsdownloads deaktiveret"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"title": "Security",
|
||||||
|
"inactivity_timeout": {
|
||||||
|
"title": "Inactivity Timeout",
|
||||||
|
"description": "Auto logout after inactivity",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"1_minute": "1 minute",
|
||||||
|
"5_minutes": "5 minutes",
|
||||||
|
"15_minutes": "15 minutes",
|
||||||
|
"30_minutes": "30 minutes",
|
||||||
|
"1_hour": "1 hour",
|
||||||
|
"4_hours": "4 hours",
|
||||||
|
"24_hours": "24 hours"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -456,6 +534,7 @@
|
|||||||
"new_app_version_requires_re_download_description": "Den nye opdatering kræver, at indhold downloades igen. Fjern venligst alt downloadet indhold og prøv igen.",
|
"new_app_version_requires_re_download_description": "Den nye opdatering kræver, at indhold downloades igen. Fjern venligst alt downloadet indhold og prøv igen.",
|
||||||
"back": "Tilbage",
|
"back": "Tilbage",
|
||||||
"delete": "Slet",
|
"delete": "Slet",
|
||||||
|
"delete_download": "Delete Download",
|
||||||
"something_went_wrong": "Noget gik galt",
|
"something_went_wrong": "Noget gik galt",
|
||||||
"could_not_get_stream_url_from_jellyfin": "Kunne ikke hente stream URL'en fra Jellyfin",
|
"could_not_get_stream_url_from_jellyfin": "Kunne ikke hente stream URL'en fra Jellyfin",
|
||||||
"eta": "ETA {{eta}}",
|
"eta": "ETA {{eta}}",
|
||||||
@@ -492,22 +571,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"no_results": "No Results",
|
||||||
"select": "Vælg",
|
"select": "Vælg",
|
||||||
"no_trailer_available": "Intet påhængskøretøj tilgængeligt",
|
"no_trailer_available": "Intet påhængskøretøj tilgængeligt",
|
||||||
"video": "Video",
|
"video": "Video",
|
||||||
"audio": "Lyd",
|
"audio": "Lyd",
|
||||||
"subtitle": "Undertekster",
|
"subtitle": "Undertekster",
|
||||||
"play": "Afspil",
|
"play": "Afspil",
|
||||||
|
"mark_as_played": "Mark as Played",
|
||||||
|
"mark_as_not_played": "Mark as not Played",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
"track": "Track",
|
"track": "Track",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
|
"stop": "Stop",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"verifying": "Verifying..."
|
"verifying": "Verifying...",
|
||||||
|
"login": "Login",
|
||||||
|
"refresh": "Refresh"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"search": "Søg...",
|
"search": "Søg...",
|
||||||
@@ -556,6 +641,7 @@
|
|||||||
"movies": "film",
|
"movies": "film",
|
||||||
"series": "serier",
|
"series": "serier",
|
||||||
"boxsets": "box sæt",
|
"boxsets": "box sæt",
|
||||||
|
"playlists": "Playlists",
|
||||||
"items": "elementer"
|
"items": "elementer"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
@@ -566,7 +652,8 @@
|
|||||||
"poster": "Plakat",
|
"poster": "Plakat",
|
||||||
"cover": "Omslag",
|
"cover": "Omslag",
|
||||||
"show_titles": "Vis titler",
|
"show_titles": "Vis titler",
|
||||||
"show_stats": "Vis statistik"
|
"show_stats": "Vis statistik",
|
||||||
|
"options_title": "Options"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"genres": "Genrer",
|
"genres": "Genrer",
|
||||||
@@ -574,7 +661,11 @@
|
|||||||
"sort_by": "Sortér efter",
|
"sort_by": "Sortér efter",
|
||||||
"filter_by": "Filter By",
|
"filter_by": "Filter By",
|
||||||
"sort_order": "Sorteringsrækkefølge",
|
"sort_order": "Sorteringsrækkefølge",
|
||||||
"tags": "Mærker"
|
"tags": "Mærker",
|
||||||
|
"all": "All",
|
||||||
|
"reset": "Reset",
|
||||||
|
"asc": "Ascending",
|
||||||
|
"desc": "Descending"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"favorites": {
|
"favorites": {
|
||||||
@@ -591,6 +682,8 @@
|
|||||||
"no_links": "Ingen links"
|
"no_links": "Ingen links"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
|
"live": "LIVE",
|
||||||
|
"mpv_player_title": "MPV Player",
|
||||||
"error": "Fejl",
|
"error": "Fejl",
|
||||||
"failed_to_get_stream_url": "Kunne ikke hente stream URL'en",
|
"failed_to_get_stream_url": "Kunne ikke hente stream URL'en",
|
||||||
"an_error_occured_while_playing_the_video": "Der opstod en fejl under afspilning af videoen. Tjek logfilerne i indstillinger.",
|
"an_error_occured_while_playing_the_video": "Der opstod en fejl under afspilning af videoen. Tjek logfilerne i indstillinger.",
|
||||||
@@ -608,7 +701,35 @@
|
|||||||
"downloaded_file_message": "Do you want to play the downloaded file?",
|
"downloaded_file_message": "Do you want to play the downloaded file?",
|
||||||
"downloaded_file_yes": "Yes",
|
"downloaded_file_yes": "Yes",
|
||||||
"downloaded_file_no": "No",
|
"downloaded_file_no": "No",
|
||||||
"downloaded_file_cancel": "Cancel"
|
"downloaded_file_cancel": "Cancel",
|
||||||
|
"swipe_down_settings": "Swipe down for settings",
|
||||||
|
"ends_at": "Ends at {{time}}",
|
||||||
|
"search_subtitles": "Search Subtitles",
|
||||||
|
"subtitle_tracks": "Tracks",
|
||||||
|
"subtitle_search": "Search & Download",
|
||||||
|
"download": "Download",
|
||||||
|
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
|
||||||
|
"using_jellyfin_server": "Using Jellyfin Server",
|
||||||
|
"language": "Language",
|
||||||
|
"results": "Results",
|
||||||
|
"searching": "Searching...",
|
||||||
|
"search_failed": "Search failed",
|
||||||
|
"no_subtitle_provider": "No subtitle provider configured on server",
|
||||||
|
"no_subtitles_found": "No subtitles found",
|
||||||
|
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
|
||||||
|
"settings": "Settings",
|
||||||
|
"skip_intro": "Skip Intro",
|
||||||
|
"skip_credits": "Skip Credits",
|
||||||
|
"stopPlayback": "Stop Playback",
|
||||||
|
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
|
||||||
|
"stopPlayingConfirm": "Are you sure you want to stop playback?",
|
||||||
|
"downloaded": "Downloaded"
|
||||||
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Næste",
|
"next_up": "Næste",
|
||||||
@@ -617,6 +738,11 @@
|
|||||||
"series": "Serier",
|
"series": "Serier",
|
||||||
"seasons": "Sæsoner",
|
"seasons": "Sæsoner",
|
||||||
"season": "Sæson",
|
"season": "Sæson",
|
||||||
|
"from_this_series": "From This Series",
|
||||||
|
"more_from_this_season": "More from this Season",
|
||||||
|
"view_series": "View Series",
|
||||||
|
"view_season": "View Season",
|
||||||
|
"select_season": "Select Season",
|
||||||
"no_episodes_for_this_season": "Ingen episoder for denne sæson",
|
"no_episodes_for_this_season": "Ingen episoder for denne sæson",
|
||||||
"overview": "Oversigt",
|
"overview": "Oversigt",
|
||||||
"more_with": "Mere med {{name}}",
|
"more_with": "Mere med {{name}}",
|
||||||
@@ -627,10 +753,21 @@
|
|||||||
"media_options": "Media Options",
|
"media_options": "Media Options",
|
||||||
"quality": "Kvalitet",
|
"quality": "Kvalitet",
|
||||||
"audio": "Lyd",
|
"audio": "Lyd",
|
||||||
"subtitles": "Undertekster",
|
"subtitles": {
|
||||||
|
"label": "Subtitle",
|
||||||
|
"none": "None",
|
||||||
|
"tracks": "Tracks"
|
||||||
|
},
|
||||||
"show_more": "Vis mere",
|
"show_more": "Vis mere",
|
||||||
"show_less": "Vis mindre",
|
"show_less": "Vis mindre",
|
||||||
|
"left": "left",
|
||||||
|
"more_info": "More Info",
|
||||||
|
"director": "Director",
|
||||||
|
"cast": "Cast",
|
||||||
|
"technical_details": "Technical Details",
|
||||||
"appeared_in": "Medvirket i",
|
"appeared_in": "Medvirket i",
|
||||||
|
"movies": "Movies",
|
||||||
|
"shows": "Shows",
|
||||||
"could_not_load_item": "Kunne ikke indlæse elementet",
|
"could_not_load_item": "Kunne ikke indlæse elementet",
|
||||||
"none": "Ingen",
|
"none": "Ingen",
|
||||||
"download": {
|
"download": {
|
||||||
@@ -641,7 +778,13 @@
|
|||||||
"download_x_item": "Download {{item_count}} elementer",
|
"download_x_item": "Download {{item_count}} elementer",
|
||||||
"download_unwatched_only": "Kun Usete",
|
"download_unwatched_only": "Kun Usete",
|
||||||
"download_button": "Hent"
|
"download_button": "Hent"
|
||||||
}
|
},
|
||||||
|
"mark_played": "Mark as Watched",
|
||||||
|
"mark_unplayed": "Mark as Unwatched",
|
||||||
|
"resume_playback": "Resume Playback",
|
||||||
|
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
|
||||||
|
"play_from_start": "Play from Start",
|
||||||
|
"continue_from": "Continue from {{time}}"
|
||||||
},
|
},
|
||||||
"live_tv": {
|
"live_tv": {
|
||||||
"next": "Næste",
|
"next": "Næste",
|
||||||
@@ -652,7 +795,18 @@
|
|||||||
"movies": "Film",
|
"movies": "Film",
|
||||||
"sports": "Sport",
|
"sports": "Sport",
|
||||||
"for_kids": "For børn",
|
"for_kids": "For børn",
|
||||||
"news": "Nyheder"
|
"news": "Nyheder",
|
||||||
|
"page_of": "Page {{current}} of {{total}}",
|
||||||
|
"no_programs": "No programs available",
|
||||||
|
"no_channels": "No channels available",
|
||||||
|
"tabs": {
|
||||||
|
"programs": "Programs",
|
||||||
|
"guide": "Guide",
|
||||||
|
"channels": "Channels",
|
||||||
|
"recordings": "Recordings",
|
||||||
|
"schedule": "Schedule",
|
||||||
|
"series": "Series"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"jellyseerr": {
|
"jellyseerr": {
|
||||||
"confirm": "Bekræft",
|
"confirm": "Bekræft",
|
||||||
@@ -697,6 +851,12 @@
|
|||||||
"decline": "Decline",
|
"decline": "Decline",
|
||||||
"requested_by": "Requested by {{user}}",
|
"requested_by": "Requested by {{user}}",
|
||||||
"unknown_user": "Unknown User",
|
"unknown_user": "Unknown User",
|
||||||
|
"select": "Select",
|
||||||
|
"request_all": "Request All",
|
||||||
|
"request_seasons": "Request Seasons",
|
||||||
|
"select_seasons": "Select Seasons",
|
||||||
|
"request_selected": "Request Selected",
|
||||||
|
"n_selected": "{{count}} selected",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"jellyseer_does_not_meet_requirements": "Jellyseerr serveren opfylder ikke minimumskravene! Opdater venligst til mindst 2.0.0",
|
"jellyseer_does_not_meet_requirements": "Jellyseerr serveren opfylder ikke minimumskravene! Opdater venligst til mindst 2.0.0",
|
||||||
"jellyseerr_test_failed": "Jellyseerr test mislykkedes. Prøv venligst igen.",
|
"jellyseerr_test_failed": "Jellyseerr test mislykkedes. Prøv venligst igen.",
|
||||||
@@ -716,7 +876,8 @@
|
|||||||
"search": "Søg",
|
"search": "Søg",
|
||||||
"library": "Bibliotek",
|
"library": "Bibliotek",
|
||||||
"custom_links": "Tilpassede links",
|
"custom_links": "Tilpassede links",
|
||||||
"favorites": "Favoritter"
|
"favorites": "Favoritter",
|
||||||
|
"settings": "Settings"
|
||||||
},
|
},
|
||||||
"music": {
|
"music": {
|
||||||
"title": "Music",
|
"title": "Music",
|
||||||
@@ -841,5 +1002,36 @@
|
|||||||
"show": "This show",
|
"show": "This show",
|
||||||
"all": "All media (default)"
|
"all": "All media (default)"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"companion_login": {
|
||||||
|
"title": "Pair with TV",
|
||||||
|
"align_qr": "Align the QR code within the frame",
|
||||||
|
"enter_code_manually": "Enter code manually",
|
||||||
|
"pairing_enter_credentials": "Enter credentials for TV",
|
||||||
|
"pairing_code_label": "Pairing code",
|
||||||
|
"server": "Server",
|
||||||
|
"authorize_button": "Authorize",
|
||||||
|
"authorizing": "Authorizing...",
|
||||||
|
"scan_again": "Scan Again",
|
||||||
|
"done": "Done",
|
||||||
|
"success_title": "Authorization Sent",
|
||||||
|
"pairing_tv_connecting": "The TV is connecting to your account",
|
||||||
|
"error_title": "Authorization Failed",
|
||||||
|
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
|
||||||
|
"error_generic": "Something went wrong. Please try again.",
|
||||||
|
"error_permission_denied": "Camera permission is required to scan QR codes.",
|
||||||
|
"login_as": "Log in as {{username}}?",
|
||||||
|
"on_server": "on {{server}}",
|
||||||
|
"use_different_user": "Use a different user",
|
||||||
|
"open_settings": "Open Settings"
|
||||||
|
},
|
||||||
|
"pairing": {
|
||||||
|
"pair_with_phone": "Pair with Phone",
|
||||||
|
"pair_with_phone_title": "Login TV",
|
||||||
|
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
|
||||||
|
"waiting_for_phone": "Waiting for phone...",
|
||||||
|
"scan_with_phone": "Scan with the Streamyfin app on your phone",
|
||||||
|
"logging_in": "Logging in...",
|
||||||
|
"logging_in_description": "Connecting to your server"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
"error_title": "Fehler",
|
"error_title": "Fehler",
|
||||||
"login_title": "Anmelden",
|
"login_title": "Anmelden",
|
||||||
"login_to_title": "Anmelden bei",
|
"login_to_title": "Anmelden bei",
|
||||||
|
"select_user": "Select a user to log in",
|
||||||
|
"add_user_to_login": "Add a user to log in",
|
||||||
|
"add_user": "Add User",
|
||||||
"username_placeholder": "Benutzername",
|
"username_placeholder": "Benutzername",
|
||||||
"password_placeholder": "Passwort",
|
"password_placeholder": "Passwort",
|
||||||
"login_button": "Anmelden",
|
"login_button": "Anmelden",
|
||||||
@@ -42,7 +45,13 @@
|
|||||||
"accounts_count": "{{count}} Konten",
|
"accounts_count": "{{count}} Konten",
|
||||||
"select_account": "Konto auswählen",
|
"select_account": "Konto auswählen",
|
||||||
"add_account": "Konto hinzufügen",
|
"add_account": "Konto hinzufügen",
|
||||||
"remove_account_description": "Hiermit werden die gespeicherten Zugangsdaten für {{username}} entfernt."
|
"remove_account_description": "Hiermit werden die gespeicherten Zugangsdaten für {{username}} entfernt.",
|
||||||
|
"remove_server": "Remove Server",
|
||||||
|
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
|
||||||
|
"select_your_server": "Select Your Server",
|
||||||
|
"add_server_to_get_started": "Add a server to get started",
|
||||||
|
"add_server": "Add Server",
|
||||||
|
"change_server": "Change Server"
|
||||||
},
|
},
|
||||||
"save_account": {
|
"save_account": {
|
||||||
"title": "Konto speichern",
|
"title": "Konto speichern",
|
||||||
@@ -86,6 +95,7 @@
|
|||||||
"oops": "Ups!",
|
"oops": "Ups!",
|
||||||
"error_message": "Etwas ist schiefgelaufen.\nBitte melde dich ab und wieder an.",
|
"error_message": "Etwas ist schiefgelaufen.\nBitte melde dich ab und wieder an.",
|
||||||
"continue_watching": "Weiterschauen",
|
"continue_watching": "Weiterschauen",
|
||||||
|
"continue": "Continue",
|
||||||
"next_up": "Als nächstes",
|
"next_up": "Als nächstes",
|
||||||
"continue_and_next_up": "\"Weiterschauen\" und \"Als Nächstes\"",
|
"continue_and_next_up": "\"Weiterschauen\" und \"Als Nächstes\"",
|
||||||
"recently_added_in": "Kürzlich hinzugefügt in {{libraryName}}",
|
"recently_added_in": "Kürzlich hinzugefügt in {{libraryName}}",
|
||||||
@@ -109,6 +119,12 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"settings_title": "Einstellungen",
|
"settings_title": "Einstellungen",
|
||||||
"log_out_button": "Abmelden",
|
"log_out_button": "Abmelden",
|
||||||
|
"switch_user": {
|
||||||
|
"title": "Switch User",
|
||||||
|
"account": "Account",
|
||||||
|
"switch_user": "Switch User on This Server",
|
||||||
|
"current": "current"
|
||||||
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "Kategorien"
|
"title": "Kategorien"
|
||||||
},
|
},
|
||||||
@@ -121,7 +137,16 @@
|
|||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "Aussehen",
|
"title": "Aussehen",
|
||||||
"merge_next_up_continue_watching": "\"Weiterschauen\" und \"Als Nächstes\" kombinieren",
|
"merge_next_up_continue_watching": "\"Weiterschauen\" und \"Als Nächstes\" kombinieren",
|
||||||
"hide_remote_session_button": "Button für Remote-Sitzung ausblenden"
|
"hide_remote_session_button": "Button für Remote-Sitzung ausblenden",
|
||||||
|
"show_home_backdrop": "Dynamic Home Backdrop",
|
||||||
|
"show_hero_carousel": "Hero Carousel",
|
||||||
|
"show_series_poster_on_episode": "Show Series Poster on Episodes",
|
||||||
|
"theme_music": "Theme Music",
|
||||||
|
"display_size": "Display Size",
|
||||||
|
"display_size_small": "Small",
|
||||||
|
"display_size_default": "Default",
|
||||||
|
"display_size_large": "Large",
|
||||||
|
"display_size_extra_large": "Extra Large"
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"title": "Netzwerk",
|
"title": "Netzwerk",
|
||||||
@@ -174,6 +199,22 @@
|
|||||||
"rewind_length": "Rückspullänge",
|
"rewind_length": "Rückspullänge",
|
||||||
"seconds_unit": "s"
|
"seconds_unit": "s"
|
||||||
},
|
},
|
||||||
|
"buffer": {
|
||||||
|
"title": "Buffer Settings",
|
||||||
|
"cache_mode": "Cache Mode",
|
||||||
|
"cache_auto": "Auto",
|
||||||
|
"cache_yes": "Enabled",
|
||||||
|
"cache_no": "Disabled",
|
||||||
|
"buffer_duration": "Buffer Duration",
|
||||||
|
"max_cache_size": "Max Cache Size",
|
||||||
|
"max_backward_cache": "Max Backward Cache"
|
||||||
|
},
|
||||||
|
"vo_driver": {
|
||||||
|
"title": "Video Output",
|
||||||
|
"vo_mode": "VO Driver",
|
||||||
|
"gpu_next": "gpu-next (Recommended)",
|
||||||
|
"gpu": "gpu"
|
||||||
|
},
|
||||||
"gesture_controls": {
|
"gesture_controls": {
|
||||||
"gesture_controls_title": "Gestensteuerung",
|
"gesture_controls_title": "Gestensteuerung",
|
||||||
"horizontal_swipe_skip": "Horizontal Wischen zum Überspringen",
|
"horizontal_swipe_skip": "Horizontal Wischen zum Überspringen",
|
||||||
@@ -256,7 +297,23 @@
|
|||||||
"subtitle_font": "Untertitel-Schriftart",
|
"subtitle_font": "Untertitel-Schriftart",
|
||||||
"ksplayer_title": "KSPlayer Einstellungen",
|
"ksplayer_title": "KSPlayer Einstellungen",
|
||||||
"hardware_decode": "Hardware Decoding",
|
"hardware_decode": "Hardware Decoding",
|
||||||
"hardware_decode_description": "Hardwarebeschleunigung für Video Decoding verwenden. Deaktivieren wenn Wiedergabeprobleme auftreten."
|
"hardware_decode_description": "Hardwarebeschleunigung für Video Decoding verwenden. Deaktivieren wenn Wiedergabeprobleme auftreten.",
|
||||||
|
"opensubtitles_title": "OpenSubtitles",
|
||||||
|
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
|
||||||
|
"opensubtitles_api_key": "API Key",
|
||||||
|
"opensubtitles_api_key_placeholder": "Enter API key...",
|
||||||
|
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
|
||||||
|
"mpv_subtitle_scale": "Subtitle Scale",
|
||||||
|
"mpv_subtitle_margin_y": "Vertical Margin",
|
||||||
|
"mpv_subtitle_align_x": "Horizontal Align",
|
||||||
|
"mpv_subtitle_align_y": "Vertical Align",
|
||||||
|
"align": {
|
||||||
|
"left": "Left",
|
||||||
|
"center": "Center",
|
||||||
|
"right": "Right",
|
||||||
|
"top": "Top",
|
||||||
|
"bottom": "Bottom"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"vlc_subtitles": {
|
"vlc_subtitles": {
|
||||||
"title": "VLC Untertitel-Einstellungen",
|
"title": "VLC Untertitel-Einstellungen",
|
||||||
@@ -406,7 +463,13 @@
|
|||||||
"music_cache_cleared": "Musik-Cache geleert",
|
"music_cache_cleared": "Musik-Cache geleert",
|
||||||
"delete_all_downloaded_songs": "Alle heruntergeladenen Titel löschen",
|
"delete_all_downloaded_songs": "Alle heruntergeladenen Titel löschen",
|
||||||
"downloaded_songs_size": "{{size}} heruntergeladen",
|
"downloaded_songs_size": "{{size}} heruntergeladen",
|
||||||
"downloaded_songs_deleted": "Heruntergeladene Titel gelöscht"
|
"downloaded_songs_deleted": "Heruntergeladene Titel gelöscht",
|
||||||
|
"clear_all_cache": "Clear All Cache",
|
||||||
|
"clear_all_cache_confirm": "Clear All Cache?",
|
||||||
|
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
|
||||||
|
"clear_all_cache_success": "Cache Cleared",
|
||||||
|
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
|
||||||
|
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "Einführung",
|
"title": "Einführung",
|
||||||
@@ -430,6 +493,21 @@
|
|||||||
"error_deleting_files": "Fehler beim Löschen von Dateien",
|
"error_deleting_files": "Fehler beim Löschen von Dateien",
|
||||||
"background_downloads_enabled": "Hintergrunddownloads aktiviert",
|
"background_downloads_enabled": "Hintergrunddownloads aktiviert",
|
||||||
"background_downloads_disabled": "Hintergrunddownloads deaktiviert"
|
"background_downloads_disabled": "Hintergrunddownloads deaktiviert"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"title": "Security",
|
||||||
|
"inactivity_timeout": {
|
||||||
|
"title": "Inactivity Timeout",
|
||||||
|
"description": "Auto logout after inactivity",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"1_minute": "1 minute",
|
||||||
|
"5_minutes": "5 minutes",
|
||||||
|
"15_minutes": "15 minutes",
|
||||||
|
"30_minutes": "30 minutes",
|
||||||
|
"1_hour": "1 hour",
|
||||||
|
"4_hours": "4 hours",
|
||||||
|
"24_hours": "24 hours"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -493,6 +571,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"no_results": "No Results",
|
||||||
"select": "Auswählen",
|
"select": "Auswählen",
|
||||||
"no_trailer_available": "Kein Trailer verfügbar",
|
"no_trailer_available": "Kein Trailer verfügbar",
|
||||||
"video": "Video",
|
"video": "Video",
|
||||||
@@ -504,13 +583,16 @@
|
|||||||
"none": "Keine",
|
"none": "Keine",
|
||||||
"track": "Spur",
|
"track": "Spur",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
|
"stop": "Stop",
|
||||||
"delete": "Löschen",
|
"delete": "Löschen",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"remove": "Entfernen",
|
"remove": "Entfernen",
|
||||||
"next": "Weiter",
|
"next": "Weiter",
|
||||||
"back": "Zurück",
|
"back": "Zurück",
|
||||||
"continue": "Fortsetzen",
|
"continue": "Fortsetzen",
|
||||||
"verifying": "Verifiziere..."
|
"verifying": "Verifiziere...",
|
||||||
|
"login": "Login",
|
||||||
|
"refresh": "Refresh"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"search": "Suchen...",
|
"search": "Suchen...",
|
||||||
@@ -559,6 +641,7 @@
|
|||||||
"movies": "Filme",
|
"movies": "Filme",
|
||||||
"series": "Serien",
|
"series": "Serien",
|
||||||
"boxsets": "Boxsets",
|
"boxsets": "Boxsets",
|
||||||
|
"playlists": "Playlists",
|
||||||
"items": "Elemente"
|
"items": "Elemente"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
@@ -569,7 +652,8 @@
|
|||||||
"poster": "Poster",
|
"poster": "Poster",
|
||||||
"cover": "Cover",
|
"cover": "Cover",
|
||||||
"show_titles": "Titel anzeigen",
|
"show_titles": "Titel anzeigen",
|
||||||
"show_stats": "Statistiken anzeigen"
|
"show_stats": "Statistiken anzeigen",
|
||||||
|
"options_title": "Options"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"genres": "Genres",
|
"genres": "Genres",
|
||||||
@@ -577,7 +661,11 @@
|
|||||||
"sort_by": "Sortieren nach",
|
"sort_by": "Sortieren nach",
|
||||||
"filter_by": "Filtern nach",
|
"filter_by": "Filtern nach",
|
||||||
"sort_order": "Sortierreihenfolge",
|
"sort_order": "Sortierreihenfolge",
|
||||||
"tags": "Tags"
|
"tags": "Tags",
|
||||||
|
"all": "All",
|
||||||
|
"reset": "Reset",
|
||||||
|
"asc": "Ascending",
|
||||||
|
"desc": "Descending"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"favorites": {
|
"favorites": {
|
||||||
@@ -594,6 +682,8 @@
|
|||||||
"no_links": "Keine Links"
|
"no_links": "Keine Links"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
|
"live": "LIVE",
|
||||||
|
"mpv_player_title": "MPV Player",
|
||||||
"error": "Fehler",
|
"error": "Fehler",
|
||||||
"failed_to_get_stream_url": "Fehler beim Abrufen der Stream-URL",
|
"failed_to_get_stream_url": "Fehler beim Abrufen der Stream-URL",
|
||||||
"an_error_occured_while_playing_the_video": "Ein Fehler ist beim Abspielen des Videos aufgetreten. Logs in den Einstellungen überprüfen.",
|
"an_error_occured_while_playing_the_video": "Ein Fehler ist beim Abspielen des Videos aufgetreten. Logs in den Einstellungen überprüfen.",
|
||||||
@@ -612,7 +702,34 @@
|
|||||||
"downloaded_file_yes": "Ja",
|
"downloaded_file_yes": "Ja",
|
||||||
"downloaded_file_no": "Nein",
|
"downloaded_file_no": "Nein",
|
||||||
"downloaded_file_cancel": "Abbrechen",
|
"downloaded_file_cancel": "Abbrechen",
|
||||||
"ends_at": "Endet um {{time}}"
|
"swipe_down_settings": "Swipe down for settings",
|
||||||
|
"ends_at": "Endet um {{time}}",
|
||||||
|
"search_subtitles": "Search Subtitles",
|
||||||
|
"subtitle_tracks": "Tracks",
|
||||||
|
"subtitle_search": "Search & Download",
|
||||||
|
"download": "Download",
|
||||||
|
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
|
||||||
|
"using_jellyfin_server": "Using Jellyfin Server",
|
||||||
|
"language": "Language",
|
||||||
|
"results": "Results",
|
||||||
|
"searching": "Searching...",
|
||||||
|
"search_failed": "Search failed",
|
||||||
|
"no_subtitle_provider": "No subtitle provider configured on server",
|
||||||
|
"no_subtitles_found": "No subtitles found",
|
||||||
|
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
|
||||||
|
"settings": "Settings",
|
||||||
|
"skip_intro": "Skip Intro",
|
||||||
|
"skip_credits": "Skip Credits",
|
||||||
|
"stopPlayback": "Stop Playback",
|
||||||
|
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
|
||||||
|
"stopPlayingConfirm": "Are you sure you want to stop playback?",
|
||||||
|
"downloaded": "Downloaded"
|
||||||
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Als Nächstes",
|
"next_up": "Als Nächstes",
|
||||||
@@ -621,6 +738,11 @@
|
|||||||
"series": "Serien",
|
"series": "Serien",
|
||||||
"seasons": "Staffeln",
|
"seasons": "Staffeln",
|
||||||
"season": "Staffel",
|
"season": "Staffel",
|
||||||
|
"from_this_series": "From This Series",
|
||||||
|
"more_from_this_season": "More from this Season",
|
||||||
|
"view_series": "View Series",
|
||||||
|
"view_season": "View Season",
|
||||||
|
"select_season": "Select Season",
|
||||||
"no_episodes_for_this_season": "Keine Episoden für diese Staffel",
|
"no_episodes_for_this_season": "Keine Episoden für diese Staffel",
|
||||||
"overview": "Überblick",
|
"overview": "Überblick",
|
||||||
"more_with": "Mehr mit {{name}}",
|
"more_with": "Mehr mit {{name}}",
|
||||||
@@ -631,10 +753,21 @@
|
|||||||
"media_options": "Medienoptionen",
|
"media_options": "Medienoptionen",
|
||||||
"quality": "Qualität",
|
"quality": "Qualität",
|
||||||
"audio": "Audio",
|
"audio": "Audio",
|
||||||
"subtitles": "Untertitel",
|
"subtitles": {
|
||||||
|
"label": "Subtitle",
|
||||||
|
"none": "None",
|
||||||
|
"tracks": "Tracks"
|
||||||
|
},
|
||||||
"show_more": "Mehr anzeigen",
|
"show_more": "Mehr anzeigen",
|
||||||
"show_less": "Weniger anzeigen",
|
"show_less": "Weniger anzeigen",
|
||||||
|
"left": "left",
|
||||||
|
"more_info": "More Info",
|
||||||
|
"director": "Director",
|
||||||
|
"cast": "Cast",
|
||||||
|
"technical_details": "Technical Details",
|
||||||
"appeared_in": "Erschien in",
|
"appeared_in": "Erschien in",
|
||||||
|
"movies": "Movies",
|
||||||
|
"shows": "Shows",
|
||||||
"could_not_load_item": "Konnte Element nicht laden",
|
"could_not_load_item": "Konnte Element nicht laden",
|
||||||
"none": "Keine",
|
"none": "Keine",
|
||||||
"download": {
|
"download": {
|
||||||
@@ -645,7 +778,13 @@
|
|||||||
"download_x_item": "{{item_count}} Elemente herunterladen",
|
"download_x_item": "{{item_count}} Elemente herunterladen",
|
||||||
"download_unwatched_only": "Nur Ungesehene",
|
"download_unwatched_only": "Nur Ungesehene",
|
||||||
"download_button": "Herunterladen"
|
"download_button": "Herunterladen"
|
||||||
}
|
},
|
||||||
|
"mark_played": "Mark as Watched",
|
||||||
|
"mark_unplayed": "Mark as Unwatched",
|
||||||
|
"resume_playback": "Resume Playback",
|
||||||
|
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
|
||||||
|
"play_from_start": "Play from Start",
|
||||||
|
"continue_from": "Continue from {{time}}"
|
||||||
},
|
},
|
||||||
"live_tv": {
|
"live_tv": {
|
||||||
"next": "Nächste",
|
"next": "Nächste",
|
||||||
@@ -656,7 +795,18 @@
|
|||||||
"movies": "Filme",
|
"movies": "Filme",
|
||||||
"sports": "Sport",
|
"sports": "Sport",
|
||||||
"for_kids": "Für Kinder",
|
"for_kids": "Für Kinder",
|
||||||
"news": "Nachrichten"
|
"news": "Nachrichten",
|
||||||
|
"page_of": "Page {{current}} of {{total}}",
|
||||||
|
"no_programs": "No programs available",
|
||||||
|
"no_channels": "No channels available",
|
||||||
|
"tabs": {
|
||||||
|
"programs": "Programs",
|
||||||
|
"guide": "Guide",
|
||||||
|
"channels": "Channels",
|
||||||
|
"recordings": "Recordings",
|
||||||
|
"schedule": "Schedule",
|
||||||
|
"series": "Series"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"jellyseerr": {
|
"jellyseerr": {
|
||||||
"confirm": "Bestätigen",
|
"confirm": "Bestätigen",
|
||||||
@@ -701,6 +851,12 @@
|
|||||||
"decline": "Ablehnen",
|
"decline": "Ablehnen",
|
||||||
"requested_by": "Angefragt von {{user}}",
|
"requested_by": "Angefragt von {{user}}",
|
||||||
"unknown_user": "Unbekannter Nutzer",
|
"unknown_user": "Unbekannter Nutzer",
|
||||||
|
"select": "Select",
|
||||||
|
"request_all": "Request All",
|
||||||
|
"request_seasons": "Request Seasons",
|
||||||
|
"select_seasons": "Select Seasons",
|
||||||
|
"request_selected": "Request Selected",
|
||||||
|
"n_selected": "{{count}} selected",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"jellyseer_does_not_meet_requirements": "Seerr-Server erfüllt nicht die minimalen Versionsanforderungen. Bitte den Seerr-Server auf mindestens 2.0.0 aktualisieren.",
|
"jellyseer_does_not_meet_requirements": "Seerr-Server erfüllt nicht die minimalen Versionsanforderungen. Bitte den Seerr-Server auf mindestens 2.0.0 aktualisieren.",
|
||||||
"jellyseerr_test_failed": "Seerr-Test fehlgeschlagen. Bitte erneut versuchen.",
|
"jellyseerr_test_failed": "Seerr-Test fehlgeschlagen. Bitte erneut versuchen.",
|
||||||
@@ -720,7 +876,8 @@
|
|||||||
"search": "Suche",
|
"search": "Suche",
|
||||||
"library": "Bibliothek",
|
"library": "Bibliothek",
|
||||||
"custom_links": "Links",
|
"custom_links": "Links",
|
||||||
"favorites": "Favoriten"
|
"favorites": "Favoriten",
|
||||||
|
"settings": "Settings"
|
||||||
},
|
},
|
||||||
"music": {
|
"music": {
|
||||||
"title": "Musik",
|
"title": "Musik",
|
||||||
@@ -845,5 +1002,36 @@
|
|||||||
"show": "Nur diese Serie",
|
"show": "Nur diese Serie",
|
||||||
"all": "Alle (Standard)"
|
"all": "Alle (Standard)"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"companion_login": {
|
||||||
|
"title": "Pair with TV",
|
||||||
|
"align_qr": "Align the QR code within the frame",
|
||||||
|
"enter_code_manually": "Enter code manually",
|
||||||
|
"pairing_enter_credentials": "Enter credentials for TV",
|
||||||
|
"pairing_code_label": "Pairing code",
|
||||||
|
"server": "Server",
|
||||||
|
"authorize_button": "Authorize",
|
||||||
|
"authorizing": "Authorizing...",
|
||||||
|
"scan_again": "Scan Again",
|
||||||
|
"done": "Done",
|
||||||
|
"success_title": "Authorization Sent",
|
||||||
|
"pairing_tv_connecting": "The TV is connecting to your account",
|
||||||
|
"error_title": "Authorization Failed",
|
||||||
|
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
|
||||||
|
"error_generic": "Something went wrong. Please try again.",
|
||||||
|
"error_permission_denied": "Camera permission is required to scan QR codes.",
|
||||||
|
"login_as": "Log in as {{username}}?",
|
||||||
|
"on_server": "on {{server}}",
|
||||||
|
"use_different_user": "Use a different user",
|
||||||
|
"open_settings": "Open Settings"
|
||||||
|
},
|
||||||
|
"pairing": {
|
||||||
|
"pair_with_phone": "Pair with Phone",
|
||||||
|
"pair_with_phone_title": "Login TV",
|
||||||
|
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
|
||||||
|
"waiting_for_phone": "Waiting for phone...",
|
||||||
|
"scan_with_phone": "Scan with the Streamyfin app on your phone",
|
||||||
|
"logging_in": "Logging in...",
|
||||||
|
"logging_in_description": "Connecting to your server"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
"error_title": "Σφάλμα",
|
"error_title": "Σφάλμα",
|
||||||
"login_title": "Σύνδεση",
|
"login_title": "Σύνδεση",
|
||||||
"login_to_title": "Συνδεθείτε στο",
|
"login_to_title": "Συνδεθείτε στο",
|
||||||
|
"select_user": "Select a user to log in",
|
||||||
|
"add_user_to_login": "Add a user to log in",
|
||||||
|
"add_user": "Add User",
|
||||||
"username_placeholder": "Όνομα Χρήστη",
|
"username_placeholder": "Όνομα Χρήστη",
|
||||||
"password_placeholder": "Κωδικός",
|
"password_placeholder": "Κωδικός",
|
||||||
"login_button": "Σύνδεση",
|
"login_button": "Σύνδεση",
|
||||||
@@ -42,7 +45,13 @@
|
|||||||
"accounts_count": "{{count}} accounts",
|
"accounts_count": "{{count}} accounts",
|
||||||
"select_account": "Select Account",
|
"select_account": "Select Account",
|
||||||
"add_account": "Add Account",
|
"add_account": "Add Account",
|
||||||
"remove_account_description": "This will remove the saved credentials for {{username}}."
|
"remove_account_description": "This will remove the saved credentials for {{username}}.",
|
||||||
|
"remove_server": "Remove Server",
|
||||||
|
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
|
||||||
|
"select_your_server": "Select Your Server",
|
||||||
|
"add_server_to_get_started": "Add a server to get started",
|
||||||
|
"add_server": "Add Server",
|
||||||
|
"change_server": "Change Server"
|
||||||
},
|
},
|
||||||
"save_account": {
|
"save_account": {
|
||||||
"title": "Save Account",
|
"title": "Save Account",
|
||||||
@@ -86,6 +95,7 @@
|
|||||||
"oops": "Ωχ!",
|
"oops": "Ωχ!",
|
||||||
"error_message": "Something went wrong.\nPlease log out and in again.",
|
"error_message": "Something went wrong.\nPlease log out and in again.",
|
||||||
"continue_watching": "Συνέχεια Παρακολούθησης",
|
"continue_watching": "Συνέχεια Παρακολούθησης",
|
||||||
|
"continue": "Continue",
|
||||||
"next_up": "Επόμενο Επάνω",
|
"next_up": "Επόμενο Επάνω",
|
||||||
"continue_and_next_up": "Continue & Next Up",
|
"continue_and_next_up": "Continue & Next Up",
|
||||||
"recently_added_in": "Προστέθηκε πρόσφατα στο {{libraryName}}",
|
"recently_added_in": "Προστέθηκε πρόσφατα στο {{libraryName}}",
|
||||||
@@ -109,6 +119,12 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"settings_title": "Ρυθμίσεις",
|
"settings_title": "Ρυθμίσεις",
|
||||||
"log_out_button": "Αποσύνδεση",
|
"log_out_button": "Αποσύνδεση",
|
||||||
|
"switch_user": {
|
||||||
|
"title": "Switch User",
|
||||||
|
"account": "Account",
|
||||||
|
"switch_user": "Switch User on This Server",
|
||||||
|
"current": "current"
|
||||||
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "Categories"
|
"title": "Categories"
|
||||||
},
|
},
|
||||||
@@ -121,7 +137,16 @@
|
|||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "Appearance",
|
"title": "Appearance",
|
||||||
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
||||||
"hide_remote_session_button": "Hide Remote Session Button"
|
"hide_remote_session_button": "Hide Remote Session Button",
|
||||||
|
"show_home_backdrop": "Dynamic Home Backdrop",
|
||||||
|
"show_hero_carousel": "Hero Carousel",
|
||||||
|
"show_series_poster_on_episode": "Show Series Poster on Episodes",
|
||||||
|
"theme_music": "Theme Music",
|
||||||
|
"display_size": "Display Size",
|
||||||
|
"display_size_small": "Small",
|
||||||
|
"display_size_default": "Default",
|
||||||
|
"display_size_large": "Large",
|
||||||
|
"display_size_extra_large": "Extra Large"
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"title": "Network",
|
"title": "Network",
|
||||||
@@ -174,6 +199,22 @@
|
|||||||
"rewind_length": "Επαναφορά Μήκους",
|
"rewind_length": "Επαναφορά Μήκους",
|
||||||
"seconds_unit": "ίνα"
|
"seconds_unit": "ίνα"
|
||||||
},
|
},
|
||||||
|
"buffer": {
|
||||||
|
"title": "Buffer Settings",
|
||||||
|
"cache_mode": "Cache Mode",
|
||||||
|
"cache_auto": "Auto",
|
||||||
|
"cache_yes": "Enabled",
|
||||||
|
"cache_no": "Disabled",
|
||||||
|
"buffer_duration": "Buffer Duration",
|
||||||
|
"max_cache_size": "Max Cache Size",
|
||||||
|
"max_backward_cache": "Max Backward Cache"
|
||||||
|
},
|
||||||
|
"vo_driver": {
|
||||||
|
"title": "Video Output",
|
||||||
|
"vo_mode": "VO Driver",
|
||||||
|
"gpu_next": "gpu-next (Recommended)",
|
||||||
|
"gpu": "gpu"
|
||||||
|
},
|
||||||
"gesture_controls": {
|
"gesture_controls": {
|
||||||
"gesture_controls_title": "Έλεγχοι Χειρονομιών",
|
"gesture_controls_title": "Έλεγχοι Χειρονομιών",
|
||||||
"horizontal_swipe_skip": "Οριζόντια σάρωση για παράλειψη",
|
"horizontal_swipe_skip": "Οριζόντια σάρωση για παράλειψη",
|
||||||
@@ -256,7 +297,23 @@
|
|||||||
"subtitle_font": "Subtitle Font",
|
"subtitle_font": "Subtitle Font",
|
||||||
"ksplayer_title": "KSPlayer Settings",
|
"ksplayer_title": "KSPlayer Settings",
|
||||||
"hardware_decode": "Hardware Decoding",
|
"hardware_decode": "Hardware Decoding",
|
||||||
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
|
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.",
|
||||||
|
"opensubtitles_title": "OpenSubtitles",
|
||||||
|
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
|
||||||
|
"opensubtitles_api_key": "API Key",
|
||||||
|
"opensubtitles_api_key_placeholder": "Enter API key...",
|
||||||
|
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
|
||||||
|
"mpv_subtitle_scale": "Subtitle Scale",
|
||||||
|
"mpv_subtitle_margin_y": "Vertical Margin",
|
||||||
|
"mpv_subtitle_align_x": "Horizontal Align",
|
||||||
|
"mpv_subtitle_align_y": "Vertical Align",
|
||||||
|
"align": {
|
||||||
|
"left": "Left",
|
||||||
|
"center": "Center",
|
||||||
|
"right": "Right",
|
||||||
|
"top": "Top",
|
||||||
|
"bottom": "Bottom"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"vlc_subtitles": {
|
"vlc_subtitles": {
|
||||||
"title": "VLC Subtitle Settings",
|
"title": "VLC Subtitle Settings",
|
||||||
@@ -406,7 +463,13 @@
|
|||||||
"music_cache_cleared": "Music cache cleared",
|
"music_cache_cleared": "Music cache cleared",
|
||||||
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
||||||
"downloaded_songs_size": "{{size}} downloaded",
|
"downloaded_songs_size": "{{size}} downloaded",
|
||||||
"downloaded_songs_deleted": "Downloaded songs deleted"
|
"downloaded_songs_deleted": "Downloaded songs deleted",
|
||||||
|
"clear_all_cache": "Clear All Cache",
|
||||||
|
"clear_all_cache_confirm": "Clear All Cache?",
|
||||||
|
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
|
||||||
|
"clear_all_cache_success": "Cache Cleared",
|
||||||
|
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
|
||||||
|
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "Intro",
|
"title": "Intro",
|
||||||
@@ -430,6 +493,21 @@
|
|||||||
"error_deleting_files": "Σφάλμα Διαγραφής Αρχείων",
|
"error_deleting_files": "Σφάλμα Διαγραφής Αρχείων",
|
||||||
"background_downloads_enabled": "Οι λήψεις στο παρασκήνιο ενεργοποιήθηκαν",
|
"background_downloads_enabled": "Οι λήψεις στο παρασκήνιο ενεργοποιήθηκαν",
|
||||||
"background_downloads_disabled": "Οι λήψεις παρασκηνίου απενεργοποιήθηκαν"
|
"background_downloads_disabled": "Οι λήψεις παρασκηνίου απενεργοποιήθηκαν"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"title": "Security",
|
||||||
|
"inactivity_timeout": {
|
||||||
|
"title": "Inactivity Timeout",
|
||||||
|
"description": "Auto logout after inactivity",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"1_minute": "1 minute",
|
||||||
|
"5_minutes": "5 minutes",
|
||||||
|
"15_minutes": "15 minutes",
|
||||||
|
"30_minutes": "30 minutes",
|
||||||
|
"1_hour": "1 hour",
|
||||||
|
"4_hours": "4 hours",
|
||||||
|
"24_hours": "24 hours"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -456,6 +534,7 @@
|
|||||||
"new_app_version_requires_re_download_description": "Η νέα ενημέρωση απαιτεί λήψη περιεχομένου ξανά. Καταργήστε όλο το κατεβασμένο περιεχόμενο και προσπαθήστε ξανά.",
|
"new_app_version_requires_re_download_description": "Η νέα ενημέρωση απαιτεί λήψη περιεχομένου ξανά. Καταργήστε όλο το κατεβασμένο περιεχόμενο και προσπαθήστε ξανά.",
|
||||||
"back": "Πίσω",
|
"back": "Πίσω",
|
||||||
"delete": "Διαγραφή",
|
"delete": "Διαγραφή",
|
||||||
|
"delete_download": "Delete Download",
|
||||||
"something_went_wrong": "Κάτι Πήγε Λάθος",
|
"something_went_wrong": "Κάτι Πήγε Λάθος",
|
||||||
"could_not_get_stream_url_from_jellyfin": "Αδυναμία λήψης του URL ροής από το Jellyfin",
|
"could_not_get_stream_url_from_jellyfin": "Αδυναμία λήψης του URL ροής από το Jellyfin",
|
||||||
"eta": "ETA {{eta}}",
|
"eta": "ETA {{eta}}",
|
||||||
@@ -492,22 +571,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"no_results": "No Results",
|
||||||
"select": "Επιλογή",
|
"select": "Επιλογή",
|
||||||
"no_trailer_available": "Δεν υπάρχει διαθέσιμο ρυμουλκούμενο",
|
"no_trailer_available": "Δεν υπάρχει διαθέσιμο ρυμουλκούμενο",
|
||||||
"video": "Βίντεο",
|
"video": "Βίντεο",
|
||||||
"audio": "Ήχος",
|
"audio": "Ήχος",
|
||||||
"subtitle": "Υπότιτλος",
|
"subtitle": "Υπότιτλος",
|
||||||
"play": "Αναπαραγωγή",
|
"play": "Αναπαραγωγή",
|
||||||
|
"mark_as_played": "Mark as Played",
|
||||||
|
"mark_as_not_played": "Mark as not Played",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
"track": "Track",
|
"track": "Track",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
|
"stop": "Stop",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"verifying": "Verifying..."
|
"verifying": "Verifying...",
|
||||||
|
"login": "Login",
|
||||||
|
"refresh": "Refresh"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"search": "Αναζήτηση...",
|
"search": "Αναζήτηση...",
|
||||||
@@ -556,6 +641,7 @@
|
|||||||
"movies": "Ταινίες",
|
"movies": "Ταινίες",
|
||||||
"series": "Σειρά",
|
"series": "Σειρά",
|
||||||
"boxsets": "Σύνολα Πλαισίων",
|
"boxsets": "Σύνολα Πλαισίων",
|
||||||
|
"playlists": "Playlists",
|
||||||
"items": "Στοιχεία"
|
"items": "Στοιχεία"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
@@ -566,7 +652,8 @@
|
|||||||
"poster": "Αφίσα",
|
"poster": "Αφίσα",
|
||||||
"cover": "Εξώφυλλο",
|
"cover": "Εξώφυλλο",
|
||||||
"show_titles": "Εμφάνιση Τίτλων",
|
"show_titles": "Εμφάνιση Τίτλων",
|
||||||
"show_stats": "Εμφάνιση Στατιστικών"
|
"show_stats": "Εμφάνιση Στατιστικών",
|
||||||
|
"options_title": "Options"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"genres": "Genres",
|
"genres": "Genres",
|
||||||
@@ -574,7 +661,11 @@
|
|||||||
"sort_by": "Ταξινόμηση Κατά",
|
"sort_by": "Ταξινόμηση Κατά",
|
||||||
"filter_by": "Filter By",
|
"filter_by": "Filter By",
|
||||||
"sort_order": "Σειρά Ταξινόμησης",
|
"sort_order": "Σειρά Ταξινόμησης",
|
||||||
"tags": "Ετικέτες"
|
"tags": "Ετικέτες",
|
||||||
|
"all": "All",
|
||||||
|
"reset": "Reset",
|
||||||
|
"asc": "Ascending",
|
||||||
|
"desc": "Descending"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"favorites": {
|
"favorites": {
|
||||||
@@ -591,6 +682,8 @@
|
|||||||
"no_links": "Δεν Υπάρχουν Σύνδεσμοι"
|
"no_links": "Δεν Υπάρχουν Σύνδεσμοι"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
|
"live": "LIVE",
|
||||||
|
"mpv_player_title": "MPV Player",
|
||||||
"error": "Σφάλμα",
|
"error": "Σφάλμα",
|
||||||
"failed_to_get_stream_url": "Αποτυχία λήψης του URL ροής",
|
"failed_to_get_stream_url": "Αποτυχία λήψης του URL ροής",
|
||||||
"an_error_occured_while_playing_the_video": "Παρουσιάστηκε σφάλμα κατά την αναπαραγωγή του βίντεο. Ελέγξτε τα αρχεία καταγραφής στις ρυθμίσεις.",
|
"an_error_occured_while_playing_the_video": "Παρουσιάστηκε σφάλμα κατά την αναπαραγωγή του βίντεο. Ελέγξτε τα αρχεία καταγραφής στις ρυθμίσεις.",
|
||||||
@@ -608,7 +701,35 @@
|
|||||||
"downloaded_file_message": "Do you want to play the downloaded file?",
|
"downloaded_file_message": "Do you want to play the downloaded file?",
|
||||||
"downloaded_file_yes": "Yes",
|
"downloaded_file_yes": "Yes",
|
||||||
"downloaded_file_no": "No",
|
"downloaded_file_no": "No",
|
||||||
"downloaded_file_cancel": "Cancel"
|
"downloaded_file_cancel": "Cancel",
|
||||||
|
"swipe_down_settings": "Swipe down for settings",
|
||||||
|
"ends_at": "Ends at {{time}}",
|
||||||
|
"search_subtitles": "Search Subtitles",
|
||||||
|
"subtitle_tracks": "Tracks",
|
||||||
|
"subtitle_search": "Search & Download",
|
||||||
|
"download": "Download",
|
||||||
|
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
|
||||||
|
"using_jellyfin_server": "Using Jellyfin Server",
|
||||||
|
"language": "Language",
|
||||||
|
"results": "Results",
|
||||||
|
"searching": "Searching...",
|
||||||
|
"search_failed": "Search failed",
|
||||||
|
"no_subtitle_provider": "No subtitle provider configured on server",
|
||||||
|
"no_subtitles_found": "No subtitles found",
|
||||||
|
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
|
||||||
|
"settings": "Settings",
|
||||||
|
"skip_intro": "Skip Intro",
|
||||||
|
"skip_credits": "Skip Credits",
|
||||||
|
"stopPlayback": "Stop Playback",
|
||||||
|
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
|
||||||
|
"stopPlayingConfirm": "Are you sure you want to stop playback?",
|
||||||
|
"downloaded": "Downloaded"
|
||||||
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Επόμενο Επάνω",
|
"next_up": "Επόμενο Επάνω",
|
||||||
@@ -617,6 +738,11 @@
|
|||||||
"series": "Σειρά",
|
"series": "Σειρά",
|
||||||
"seasons": "Περίοδοι",
|
"seasons": "Περίοδοι",
|
||||||
"season": "Σεζόν",
|
"season": "Σεζόν",
|
||||||
|
"from_this_series": "From This Series",
|
||||||
|
"more_from_this_season": "More from this Season",
|
||||||
|
"view_series": "View Series",
|
||||||
|
"view_season": "View Season",
|
||||||
|
"select_season": "Select Season",
|
||||||
"no_episodes_for_this_season": "Δεν υπάρχουν επεισόδια για αυτή τη σεζόν",
|
"no_episodes_for_this_season": "Δεν υπάρχουν επεισόδια για αυτή τη σεζόν",
|
||||||
"overview": "Επισκόπηση",
|
"overview": "Επισκόπηση",
|
||||||
"more_with": "More with {{name}}",
|
"more_with": "More with {{name}}",
|
||||||
@@ -627,10 +753,21 @@
|
|||||||
"media_options": "Media Options",
|
"media_options": "Media Options",
|
||||||
"quality": "Ποιότητα",
|
"quality": "Ποιότητα",
|
||||||
"audio": "Ήχος",
|
"audio": "Ήχος",
|
||||||
"subtitles": "Υπότιτλος",
|
"subtitles": {
|
||||||
|
"label": "Subtitle",
|
||||||
|
"none": "None",
|
||||||
|
"tracks": "Tracks"
|
||||||
|
},
|
||||||
"show_more": "Εμφάνιση Περισσότερων",
|
"show_more": "Εμφάνιση Περισσότερων",
|
||||||
"show_less": "Εμφάνιση Λιγότερων",
|
"show_less": "Εμφάνιση Λιγότερων",
|
||||||
|
"left": "left",
|
||||||
|
"more_info": "More Info",
|
||||||
|
"director": "Director",
|
||||||
|
"cast": "Cast",
|
||||||
|
"technical_details": "Technical Details",
|
||||||
"appeared_in": "Εμφανίστηκε Σε",
|
"appeared_in": "Εμφανίστηκε Σε",
|
||||||
|
"movies": "Movies",
|
||||||
|
"shows": "Shows",
|
||||||
"could_not_load_item": "Αδύνατη Η Φόρτωση Του Στοιχείου",
|
"could_not_load_item": "Αδύνατη Η Φόρτωση Του Στοιχείου",
|
||||||
"none": "Κανένα",
|
"none": "Κανένα",
|
||||||
"download": {
|
"download": {
|
||||||
@@ -641,7 +778,13 @@
|
|||||||
"download_x_item": "Λήψη Αντικειμένων {{item_count}}",
|
"download_x_item": "Λήψη Αντικειμένων {{item_count}}",
|
||||||
"download_unwatched_only": "Μόνο Χωρίς Παρακολούθηση",
|
"download_unwatched_only": "Μόνο Χωρίς Παρακολούθηση",
|
||||||
"download_button": "Λήψη"
|
"download_button": "Λήψη"
|
||||||
}
|
},
|
||||||
|
"mark_played": "Mark as Watched",
|
||||||
|
"mark_unplayed": "Mark as Unwatched",
|
||||||
|
"resume_playback": "Resume Playback",
|
||||||
|
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
|
||||||
|
"play_from_start": "Play from Start",
|
||||||
|
"continue_from": "Continue from {{time}}"
|
||||||
},
|
},
|
||||||
"live_tv": {
|
"live_tv": {
|
||||||
"next": "Επόμενο",
|
"next": "Επόμενο",
|
||||||
@@ -652,7 +795,18 @@
|
|||||||
"movies": "Ταινίες",
|
"movies": "Ταινίες",
|
||||||
"sports": "Αθλητισμός",
|
"sports": "Αθλητισμός",
|
||||||
"for_kids": "Για Παιδιά",
|
"for_kids": "Για Παιδιά",
|
||||||
"news": "Νέα"
|
"news": "Νέα",
|
||||||
|
"page_of": "Page {{current}} of {{total}}",
|
||||||
|
"no_programs": "No programs available",
|
||||||
|
"no_channels": "No channels available",
|
||||||
|
"tabs": {
|
||||||
|
"programs": "Programs",
|
||||||
|
"guide": "Guide",
|
||||||
|
"channels": "Channels",
|
||||||
|
"recordings": "Recordings",
|
||||||
|
"schedule": "Schedule",
|
||||||
|
"series": "Series"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"jellyseerr": {
|
"jellyseerr": {
|
||||||
"confirm": "Επιβεβαίωση",
|
"confirm": "Επιβεβαίωση",
|
||||||
@@ -697,6 +851,12 @@
|
|||||||
"decline": "Decline",
|
"decline": "Decline",
|
||||||
"requested_by": "Requested by {{user}}",
|
"requested_by": "Requested by {{user}}",
|
||||||
"unknown_user": "Unknown User",
|
"unknown_user": "Unknown User",
|
||||||
|
"select": "Select",
|
||||||
|
"request_all": "Request All",
|
||||||
|
"request_seasons": "Request Seasons",
|
||||||
|
"select_seasons": "Select Seasons",
|
||||||
|
"request_selected": "Request Selected",
|
||||||
|
"n_selected": "{{count}} selected",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"jellyseer_does_not_meet_requirements": "Ο διακομιστής Seerr δεν πληροί τις ελάχιστες απαιτήσεις έκδοσης! Παρακαλούμε ενημερώστε τουλάχιστον σε 2.0.0",
|
"jellyseer_does_not_meet_requirements": "Ο διακομιστής Seerr δεν πληροί τις ελάχιστες απαιτήσεις έκδοσης! Παρακαλούμε ενημερώστε τουλάχιστον σε 2.0.0",
|
||||||
"jellyseerr_test_failed": "Ο έλεγχος Seerr απέτυχε. Παρακαλώ προσπαθήστε ξανά.",
|
"jellyseerr_test_failed": "Ο έλεγχος Seerr απέτυχε. Παρακαλώ προσπαθήστε ξανά.",
|
||||||
@@ -716,7 +876,8 @@
|
|||||||
"search": "Αναζήτηση",
|
"search": "Αναζήτηση",
|
||||||
"library": "Βιβλιοθήκη",
|
"library": "Βιβλιοθήκη",
|
||||||
"custom_links": "Προσαρμοσμένοι Σύνδεσμοι",
|
"custom_links": "Προσαρμοσμένοι Σύνδεσμοι",
|
||||||
"favorites": "Αγαπημένα"
|
"favorites": "Αγαπημένα",
|
||||||
|
"settings": "Settings"
|
||||||
},
|
},
|
||||||
"music": {
|
"music": {
|
||||||
"title": "Music",
|
"title": "Music",
|
||||||
@@ -841,5 +1002,36 @@
|
|||||||
"show": "This show",
|
"show": "This show",
|
||||||
"all": "All media (default)"
|
"all": "All media (default)"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"companion_login": {
|
||||||
|
"title": "Pair with TV",
|
||||||
|
"align_qr": "Align the QR code within the frame",
|
||||||
|
"enter_code_manually": "Enter code manually",
|
||||||
|
"pairing_enter_credentials": "Enter credentials for TV",
|
||||||
|
"pairing_code_label": "Pairing code",
|
||||||
|
"server": "Server",
|
||||||
|
"authorize_button": "Authorize",
|
||||||
|
"authorizing": "Authorizing...",
|
||||||
|
"scan_again": "Scan Again",
|
||||||
|
"done": "Done",
|
||||||
|
"success_title": "Authorization Sent",
|
||||||
|
"pairing_tv_connecting": "The TV is connecting to your account",
|
||||||
|
"error_title": "Authorization Failed",
|
||||||
|
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
|
||||||
|
"error_generic": "Something went wrong. Please try again.",
|
||||||
|
"error_permission_denied": "Camera permission is required to scan QR codes.",
|
||||||
|
"login_as": "Log in as {{username}}?",
|
||||||
|
"on_server": "on {{server}}",
|
||||||
|
"use_different_user": "Use a different user",
|
||||||
|
"open_settings": "Open Settings"
|
||||||
|
},
|
||||||
|
"pairing": {
|
||||||
|
"pair_with_phone": "Pair with Phone",
|
||||||
|
"pair_with_phone_title": "Login TV",
|
||||||
|
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
|
||||||
|
"waiting_for_phone": "Waiting for phone...",
|
||||||
|
"scan_with_phone": "Scan with the Streamyfin app on your phone",
|
||||||
|
"logging_in": "Logging in...",
|
||||||
|
"logging_in_description": "Connecting to your server"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -652,7 +652,8 @@
|
|||||||
"poster": "Poster",
|
"poster": "Poster",
|
||||||
"cover": "Cover",
|
"cover": "Cover",
|
||||||
"show_titles": "Show Titles",
|
"show_titles": "Show Titles",
|
||||||
"show_stats": "Show Stats"
|
"show_stats": "Show Stats",
|
||||||
|
"options_title": "Options"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"genres": "Genres",
|
"genres": "Genres",
|
||||||
@@ -682,6 +683,7 @@
|
|||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
"live": "LIVE",
|
"live": "LIVE",
|
||||||
|
"mpv_player_title": "MPV Player",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"failed_to_get_stream_url": "Failed to get the stream URL",
|
"failed_to_get_stream_url": "Failed to get the stream URL",
|
||||||
"an_error_occured_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.",
|
"an_error_occured_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.",
|
||||||
@@ -1001,6 +1003,30 @@
|
|||||||
"all": "All media (default)"
|
"all": "All media (default)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"syncplay": {
|
||||||
|
"title": "SyncPlay",
|
||||||
|
"my_group": "My Group",
|
||||||
|
"join_group": "Join Group",
|
||||||
|
"leave_group": "Leave Group",
|
||||||
|
"create_group": "Create Group",
|
||||||
|
"create_new_group": "Create New Group",
|
||||||
|
"available_groups": "Available Groups",
|
||||||
|
"group_id": "Group ID",
|
||||||
|
"leader": "Leader",
|
||||||
|
"members": "members",
|
||||||
|
"enabled": "SyncPlay enabled",
|
||||||
|
"disabled": "SyncPlay disabled",
|
||||||
|
"user_joined": "{{username}} joined the group",
|
||||||
|
"user_left": "{{username}} left the group",
|
||||||
|
"permission_required": "Permission required to use SyncPlay",
|
||||||
|
"group_does_not_exist": "Group does not exist",
|
||||||
|
"create_denied": "Permission denied to create group",
|
||||||
|
"join_denied": "Permission denied to join group",
|
||||||
|
"library_access_denied": "Library access denied",
|
||||||
|
"waiting_for_group": "Waiting for group...",
|
||||||
|
"joining_playback": "Joining group playback...",
|
||||||
|
"failed_to_start": "Failed to start SyncPlay group playback"
|
||||||
|
},
|
||||||
"companion_login": {
|
"companion_login": {
|
||||||
"title": "Pair with TV",
|
"title": "Pair with TV",
|
||||||
"align_qr": "Align the QR code within the frame",
|
"align_qr": "Align the QR code within the frame",
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
"error_title": "Error",
|
"error_title": "Error",
|
||||||
"login_title": "Iniciar sesión",
|
"login_title": "Iniciar sesión",
|
||||||
"login_to_title": "Iniciar sesión en",
|
"login_to_title": "Iniciar sesión en",
|
||||||
|
"select_user": "Select a user to log in",
|
||||||
|
"add_user_to_login": "Add a user to log in",
|
||||||
|
"add_user": "Add User",
|
||||||
"username_placeholder": "Nombre de usuario",
|
"username_placeholder": "Nombre de usuario",
|
||||||
"password_placeholder": "Contraseña",
|
"password_placeholder": "Contraseña",
|
||||||
"login_button": "Iniciar sesión",
|
"login_button": "Iniciar sesión",
|
||||||
@@ -42,7 +45,13 @@
|
|||||||
"accounts_count": "{{count}} cuentas",
|
"accounts_count": "{{count}} cuentas",
|
||||||
"select_account": "Seleccione una cuenta",
|
"select_account": "Seleccione una cuenta",
|
||||||
"add_account": "Añadir cuenta",
|
"add_account": "Añadir cuenta",
|
||||||
"remove_account_description": "Esto eliminará las credenciales guardadas para {{username}}."
|
"remove_account_description": "Esto eliminará las credenciales guardadas para {{username}}.",
|
||||||
|
"remove_server": "Remove Server",
|
||||||
|
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
|
||||||
|
"select_your_server": "Select Your Server",
|
||||||
|
"add_server_to_get_started": "Add a server to get started",
|
||||||
|
"add_server": "Add Server",
|
||||||
|
"change_server": "Change Server"
|
||||||
},
|
},
|
||||||
"save_account": {
|
"save_account": {
|
||||||
"title": "Guardar Cuenta",
|
"title": "Guardar Cuenta",
|
||||||
@@ -86,6 +95,7 @@
|
|||||||
"oops": "¡Vaya!",
|
"oops": "¡Vaya!",
|
||||||
"error_message": "Algo ha salido mal.\nPor favor, cierra la sesión y vuelve a iniciar.",
|
"error_message": "Algo ha salido mal.\nPor favor, cierra la sesión y vuelve a iniciar.",
|
||||||
"continue_watching": "Seguir viendo",
|
"continue_watching": "Seguir viendo",
|
||||||
|
"continue": "Continue",
|
||||||
"next_up": "A continuación",
|
"next_up": "A continuación",
|
||||||
"continue_and_next_up": "Continuar y siguiente",
|
"continue_and_next_up": "Continuar y siguiente",
|
||||||
"recently_added_in": "Recientemente añadido en {{libraryName}}",
|
"recently_added_in": "Recientemente añadido en {{libraryName}}",
|
||||||
@@ -109,6 +119,12 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"settings_title": "Configuración",
|
"settings_title": "Configuración",
|
||||||
"log_out_button": "Cerrar sesión",
|
"log_out_button": "Cerrar sesión",
|
||||||
|
"switch_user": {
|
||||||
|
"title": "Switch User",
|
||||||
|
"account": "Account",
|
||||||
|
"switch_user": "Switch User on This Server",
|
||||||
|
"current": "current"
|
||||||
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "Categorías"
|
"title": "Categorías"
|
||||||
},
|
},
|
||||||
@@ -121,7 +137,16 @@
|
|||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "Apariencia",
|
"title": "Apariencia",
|
||||||
"merge_next_up_continue_watching": "Fusionar continuar viendo y siguiente",
|
"merge_next_up_continue_watching": "Fusionar continuar viendo y siguiente",
|
||||||
"hide_remote_session_button": "Ocultar botón de sesión remota"
|
"hide_remote_session_button": "Ocultar botón de sesión remota",
|
||||||
|
"show_home_backdrop": "Dynamic Home Backdrop",
|
||||||
|
"show_hero_carousel": "Hero Carousel",
|
||||||
|
"show_series_poster_on_episode": "Show Series Poster on Episodes",
|
||||||
|
"theme_music": "Theme Music",
|
||||||
|
"display_size": "Display Size",
|
||||||
|
"display_size_small": "Small",
|
||||||
|
"display_size_default": "Default",
|
||||||
|
"display_size_large": "Large",
|
||||||
|
"display_size_extra_large": "Extra Large"
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"title": "Cadena",
|
"title": "Cadena",
|
||||||
@@ -174,6 +199,22 @@
|
|||||||
"rewind_length": "Longitud de retroceso",
|
"rewind_length": "Longitud de retroceso",
|
||||||
"seconds_unit": "s"
|
"seconds_unit": "s"
|
||||||
},
|
},
|
||||||
|
"buffer": {
|
||||||
|
"title": "Buffer Settings",
|
||||||
|
"cache_mode": "Cache Mode",
|
||||||
|
"cache_auto": "Auto",
|
||||||
|
"cache_yes": "Enabled",
|
||||||
|
"cache_no": "Disabled",
|
||||||
|
"buffer_duration": "Buffer Duration",
|
||||||
|
"max_cache_size": "Max Cache Size",
|
||||||
|
"max_backward_cache": "Max Backward Cache"
|
||||||
|
},
|
||||||
|
"vo_driver": {
|
||||||
|
"title": "Video Output",
|
||||||
|
"vo_mode": "VO Driver",
|
||||||
|
"gpu_next": "gpu-next (Recommended)",
|
||||||
|
"gpu": "gpu"
|
||||||
|
},
|
||||||
"gesture_controls": {
|
"gesture_controls": {
|
||||||
"gesture_controls_title": "Controles de gestos",
|
"gesture_controls_title": "Controles de gestos",
|
||||||
"horizontal_swipe_skip": "Deslizar horizontal para omitir",
|
"horizontal_swipe_skip": "Deslizar horizontal para omitir",
|
||||||
@@ -256,7 +297,23 @@
|
|||||||
"subtitle_font": "Fuente de los subtítulos",
|
"subtitle_font": "Fuente de los subtítulos",
|
||||||
"ksplayer_title": "Ajustes de KSPlayer",
|
"ksplayer_title": "Ajustes de KSPlayer",
|
||||||
"hardware_decode": "Decodificación de hardware",
|
"hardware_decode": "Decodificación de hardware",
|
||||||
"hardware_decode_description": "Utilizar la aceleración de hardware para la decodificación de vídeo. Deshabilite si experimenta problemas de reproducción."
|
"hardware_decode_description": "Utilizar la aceleración de hardware para la decodificación de vídeo. Deshabilite si experimenta problemas de reproducción.",
|
||||||
|
"opensubtitles_title": "OpenSubtitles",
|
||||||
|
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
|
||||||
|
"opensubtitles_api_key": "API Key",
|
||||||
|
"opensubtitles_api_key_placeholder": "Enter API key...",
|
||||||
|
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
|
||||||
|
"mpv_subtitle_scale": "Subtitle Scale",
|
||||||
|
"mpv_subtitle_margin_y": "Vertical Margin",
|
||||||
|
"mpv_subtitle_align_x": "Horizontal Align",
|
||||||
|
"mpv_subtitle_align_y": "Vertical Align",
|
||||||
|
"align": {
|
||||||
|
"left": "Left",
|
||||||
|
"center": "Center",
|
||||||
|
"right": "Right",
|
||||||
|
"top": "Top",
|
||||||
|
"bottom": "Bottom"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"vlc_subtitles": {
|
"vlc_subtitles": {
|
||||||
"title": "Configuración de subtítulos VLC",
|
"title": "Configuración de subtítulos VLC",
|
||||||
@@ -406,7 +463,13 @@
|
|||||||
"music_cache_cleared": "Caché de música eliminado",
|
"music_cache_cleared": "Caché de música eliminado",
|
||||||
"delete_all_downloaded_songs": "Eliminar todas las descargas",
|
"delete_all_downloaded_songs": "Eliminar todas las descargas",
|
||||||
"downloaded_songs_size": "{{tamaño}} descargado",
|
"downloaded_songs_size": "{{tamaño}} descargado",
|
||||||
"downloaded_songs_deleted": "Canciones descargadas eliminadas"
|
"downloaded_songs_deleted": "Canciones descargadas eliminadas",
|
||||||
|
"clear_all_cache": "Clear All Cache",
|
||||||
|
"clear_all_cache_confirm": "Clear All Cache?",
|
||||||
|
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
|
||||||
|
"clear_all_cache_success": "Cache Cleared",
|
||||||
|
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
|
||||||
|
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "Intro",
|
"title": "Intro",
|
||||||
@@ -430,6 +493,21 @@
|
|||||||
"error_deleting_files": "Error al eliminar archivos",
|
"error_deleting_files": "Error al eliminar archivos",
|
||||||
"background_downloads_enabled": "Descargas en segundo plano habilitadas",
|
"background_downloads_enabled": "Descargas en segundo plano habilitadas",
|
||||||
"background_downloads_disabled": "Descargas en segundo plano deshabilitadas"
|
"background_downloads_disabled": "Descargas en segundo plano deshabilitadas"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"title": "Security",
|
||||||
|
"inactivity_timeout": {
|
||||||
|
"title": "Inactivity Timeout",
|
||||||
|
"description": "Auto logout after inactivity",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"1_minute": "1 minute",
|
||||||
|
"5_minutes": "5 minutes",
|
||||||
|
"15_minutes": "15 minutes",
|
||||||
|
"30_minutes": "30 minutes",
|
||||||
|
"1_hour": "1 hour",
|
||||||
|
"4_hours": "4 hours",
|
||||||
|
"24_hours": "24 hours"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -456,6 +534,7 @@
|
|||||||
"new_app_version_requires_re_download_description": "La nueva actualización requiere volver a descargar el contenido. Por favor, elimina todo el código descargado y vuélvelo a intentar.",
|
"new_app_version_requires_re_download_description": "La nueva actualización requiere volver a descargar el contenido. Por favor, elimina todo el código descargado y vuélvelo a intentar.",
|
||||||
"back": "Atrás",
|
"back": "Atrás",
|
||||||
"delete": "Borrar",
|
"delete": "Borrar",
|
||||||
|
"delete_download": "Delete Download",
|
||||||
"something_went_wrong": "Algo ha salido mal",
|
"something_went_wrong": "Algo ha salido mal",
|
||||||
"could_not_get_stream_url_from_jellyfin": "No se pudo obtener la URL del stream de Jellyfin",
|
"could_not_get_stream_url_from_jellyfin": "No se pudo obtener la URL del stream de Jellyfin",
|
||||||
"eta": "{{eta}} restante",
|
"eta": "{{eta}} restante",
|
||||||
@@ -492,22 +571,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"no_results": "No Results",
|
||||||
"select": "Seleccionar",
|
"select": "Seleccionar",
|
||||||
"no_trailer_available": "No hay tráiler disponible",
|
"no_trailer_available": "No hay tráiler disponible",
|
||||||
"video": "Vídeo",
|
"video": "Vídeo",
|
||||||
"audio": "Audio",
|
"audio": "Audio",
|
||||||
"subtitle": "Subtítulos",
|
"subtitle": "Subtítulos",
|
||||||
"play": "Jugar",
|
"play": "Jugar",
|
||||||
|
"mark_as_played": "Mark as Played",
|
||||||
|
"mark_as_not_played": "Mark as not Played",
|
||||||
"none": "Nada",
|
"none": "Nada",
|
||||||
"track": "Pista",
|
"track": "Pista",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
|
"stop": "Stop",
|
||||||
"delete": "Borrar",
|
"delete": "Borrar",
|
||||||
"ok": "Aceptar",
|
"ok": "Aceptar",
|
||||||
"remove": "Eliminar",
|
"remove": "Eliminar",
|
||||||
"next": "Siguiente",
|
"next": "Siguiente",
|
||||||
"back": "Atrás",
|
"back": "Atrás",
|
||||||
"continue": "Continuar",
|
"continue": "Continuar",
|
||||||
"verifying": "Verificando..."
|
"verifying": "Verificando...",
|
||||||
|
"login": "Login",
|
||||||
|
"refresh": "Refresh"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"search": "Buscar...",
|
"search": "Buscar...",
|
||||||
@@ -556,6 +641,7 @@
|
|||||||
"movies": "Películas",
|
"movies": "Películas",
|
||||||
"series": "Series",
|
"series": "Series",
|
||||||
"boxsets": "Colecciones",
|
"boxsets": "Colecciones",
|
||||||
|
"playlists": "Playlists",
|
||||||
"items": "Elementos"
|
"items": "Elementos"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
@@ -566,7 +652,8 @@
|
|||||||
"poster": "Póster",
|
"poster": "Póster",
|
||||||
"cover": "Portada",
|
"cover": "Portada",
|
||||||
"show_titles": "Mostrar títulos",
|
"show_titles": "Mostrar títulos",
|
||||||
"show_stats": "Mostrar estadísticas"
|
"show_stats": "Mostrar estadísticas",
|
||||||
|
"options_title": "Options"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"genres": "Géneros",
|
"genres": "Géneros",
|
||||||
@@ -574,7 +661,11 @@
|
|||||||
"sort_by": "Ordenar por",
|
"sort_by": "Ordenar por",
|
||||||
"filter_by": "Filtrar por",
|
"filter_by": "Filtrar por",
|
||||||
"sort_order": "Ordenar",
|
"sort_order": "Ordenar",
|
||||||
"tags": "Etiquetas"
|
"tags": "Etiquetas",
|
||||||
|
"all": "All",
|
||||||
|
"reset": "Reset",
|
||||||
|
"asc": "Ascending",
|
||||||
|
"desc": "Descending"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"favorites": {
|
"favorites": {
|
||||||
@@ -591,6 +682,8 @@
|
|||||||
"no_links": "Sin enlaces"
|
"no_links": "Sin enlaces"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
|
"live": "LIVE",
|
||||||
|
"mpv_player_title": "MPV Player",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"failed_to_get_stream_url": "Error al obtener la URL del Steam",
|
"failed_to_get_stream_url": "Error al obtener la URL del Steam",
|
||||||
"an_error_occured_while_playing_the_video": "Ha ocurrido un error al reproducir el vídeo. Comprueba los registros en la configuración.",
|
"an_error_occured_while_playing_the_video": "Ha ocurrido un error al reproducir el vídeo. Comprueba los registros en la configuración.",
|
||||||
@@ -608,7 +701,35 @@
|
|||||||
"downloaded_file_message": "¿Quieres reproducir el archivo descargado?",
|
"downloaded_file_message": "¿Quieres reproducir el archivo descargado?",
|
||||||
"downloaded_file_yes": "Sí",
|
"downloaded_file_yes": "Sí",
|
||||||
"downloaded_file_no": "No",
|
"downloaded_file_no": "No",
|
||||||
"downloaded_file_cancel": "Cancelar"
|
"downloaded_file_cancel": "Cancelar",
|
||||||
|
"swipe_down_settings": "Swipe down for settings",
|
||||||
|
"ends_at": "Ends at {{time}}",
|
||||||
|
"search_subtitles": "Search Subtitles",
|
||||||
|
"subtitle_tracks": "Tracks",
|
||||||
|
"subtitle_search": "Search & Download",
|
||||||
|
"download": "Download",
|
||||||
|
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
|
||||||
|
"using_jellyfin_server": "Using Jellyfin Server",
|
||||||
|
"language": "Language",
|
||||||
|
"results": "Results",
|
||||||
|
"searching": "Searching...",
|
||||||
|
"search_failed": "Search failed",
|
||||||
|
"no_subtitle_provider": "No subtitle provider configured on server",
|
||||||
|
"no_subtitles_found": "No subtitles found",
|
||||||
|
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
|
||||||
|
"settings": "Settings",
|
||||||
|
"skip_intro": "Skip Intro",
|
||||||
|
"skip_credits": "Skip Credits",
|
||||||
|
"stopPlayback": "Stop Playback",
|
||||||
|
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
|
||||||
|
"stopPlayingConfirm": "Are you sure you want to stop playback?",
|
||||||
|
"downloaded": "Downloaded"
|
||||||
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "A continuación",
|
"next_up": "A continuación",
|
||||||
@@ -617,6 +738,11 @@
|
|||||||
"series": "Series",
|
"series": "Series",
|
||||||
"seasons": "Temporadas",
|
"seasons": "Temporadas",
|
||||||
"season": "Temporada",
|
"season": "Temporada",
|
||||||
|
"from_this_series": "From This Series",
|
||||||
|
"more_from_this_season": "More from this Season",
|
||||||
|
"view_series": "View Series",
|
||||||
|
"view_season": "View Season",
|
||||||
|
"select_season": "Select Season",
|
||||||
"no_episodes_for_this_season": "No hay episodios para esta temporada",
|
"no_episodes_for_this_season": "No hay episodios para esta temporada",
|
||||||
"overview": "Resumen",
|
"overview": "Resumen",
|
||||||
"more_with": "Más con {{name}}",
|
"more_with": "Más con {{name}}",
|
||||||
@@ -627,10 +753,21 @@
|
|||||||
"media_options": "Opciones de medios",
|
"media_options": "Opciones de medios",
|
||||||
"quality": "Calidad",
|
"quality": "Calidad",
|
||||||
"audio": "Audio",
|
"audio": "Audio",
|
||||||
"subtitles": "Subtítulos",
|
"subtitles": {
|
||||||
|
"label": "Subtitle",
|
||||||
|
"none": "None",
|
||||||
|
"tracks": "Tracks"
|
||||||
|
},
|
||||||
"show_more": "Mostrar más",
|
"show_more": "Mostrar más",
|
||||||
"show_less": "Mostrar menos",
|
"show_less": "Mostrar menos",
|
||||||
|
"left": "left",
|
||||||
|
"more_info": "More Info",
|
||||||
|
"director": "Director",
|
||||||
|
"cast": "Cast",
|
||||||
|
"technical_details": "Technical Details",
|
||||||
"appeared_in": "Apareció en",
|
"appeared_in": "Apareció en",
|
||||||
|
"movies": "Movies",
|
||||||
|
"shows": "Shows",
|
||||||
"could_not_load_item": "No se pudo cargar el ítem",
|
"could_not_load_item": "No se pudo cargar el ítem",
|
||||||
"none": "Ninguno",
|
"none": "Ninguno",
|
||||||
"download": {
|
"download": {
|
||||||
@@ -641,7 +778,13 @@
|
|||||||
"download_x_item": "Descargar {{item_count}} ítems",
|
"download_x_item": "Descargar {{item_count}} ítems",
|
||||||
"download_unwatched_only": "No visto",
|
"download_unwatched_only": "No visto",
|
||||||
"download_button": "Descargar"
|
"download_button": "Descargar"
|
||||||
}
|
},
|
||||||
|
"mark_played": "Mark as Watched",
|
||||||
|
"mark_unplayed": "Mark as Unwatched",
|
||||||
|
"resume_playback": "Resume Playback",
|
||||||
|
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
|
||||||
|
"play_from_start": "Play from Start",
|
||||||
|
"continue_from": "Continue from {{time}}"
|
||||||
},
|
},
|
||||||
"live_tv": {
|
"live_tv": {
|
||||||
"next": "Siguiente",
|
"next": "Siguiente",
|
||||||
@@ -652,7 +795,18 @@
|
|||||||
"movies": "Películas",
|
"movies": "Películas",
|
||||||
"sports": "Deportes",
|
"sports": "Deportes",
|
||||||
"for_kids": "Para niños",
|
"for_kids": "Para niños",
|
||||||
"news": "Noticias"
|
"news": "Noticias",
|
||||||
|
"page_of": "Page {{current}} of {{total}}",
|
||||||
|
"no_programs": "No programs available",
|
||||||
|
"no_channels": "No channels available",
|
||||||
|
"tabs": {
|
||||||
|
"programs": "Programs",
|
||||||
|
"guide": "Guide",
|
||||||
|
"channels": "Channels",
|
||||||
|
"recordings": "Recordings",
|
||||||
|
"schedule": "Schedule",
|
||||||
|
"series": "Series"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"jellyseerr": {
|
"jellyseerr": {
|
||||||
"confirm": "Confirmar",
|
"confirm": "Confirmar",
|
||||||
@@ -697,6 +851,12 @@
|
|||||||
"decline": "Rechazar",
|
"decline": "Rechazar",
|
||||||
"requested_by": "Solicitado por {{user}}",
|
"requested_by": "Solicitado por {{user}}",
|
||||||
"unknown_user": "Usuario desconocido",
|
"unknown_user": "Usuario desconocido",
|
||||||
|
"select": "Select",
|
||||||
|
"request_all": "Request All",
|
||||||
|
"request_seasons": "Request Seasons",
|
||||||
|
"select_seasons": "Select Seasons",
|
||||||
|
"request_selected": "Request Selected",
|
||||||
|
"n_selected": "{{count}} selected",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"jellyseer_does_not_meet_requirements": "¡Jellyseer no cumple con los requisitos! Por favor, actualízalo al menos a la versión 2.0.0.",
|
"jellyseer_does_not_meet_requirements": "¡Jellyseer no cumple con los requisitos! Por favor, actualízalo al menos a la versión 2.0.0.",
|
||||||
"jellyseerr_test_failed": "La prueba de Jellyseerr ha fallado. Por favor inténtalo de nuevo.",
|
"jellyseerr_test_failed": "La prueba de Jellyseerr ha fallado. Por favor inténtalo de nuevo.",
|
||||||
@@ -716,7 +876,8 @@
|
|||||||
"search": "Buscar",
|
"search": "Buscar",
|
||||||
"library": "Bibliotecas",
|
"library": "Bibliotecas",
|
||||||
"custom_links": "Enlaces personalizados",
|
"custom_links": "Enlaces personalizados",
|
||||||
"favorites": "Favoritos"
|
"favorites": "Favoritos",
|
||||||
|
"settings": "Settings"
|
||||||
},
|
},
|
||||||
"music": {
|
"music": {
|
||||||
"title": "Música",
|
"title": "Música",
|
||||||
@@ -841,5 +1002,36 @@
|
|||||||
"show": "Este programa",
|
"show": "Este programa",
|
||||||
"all": "Todos los medios (por defecto)"
|
"all": "Todos los medios (por defecto)"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"companion_login": {
|
||||||
|
"title": "Pair with TV",
|
||||||
|
"align_qr": "Align the QR code within the frame",
|
||||||
|
"enter_code_manually": "Enter code manually",
|
||||||
|
"pairing_enter_credentials": "Enter credentials for TV",
|
||||||
|
"pairing_code_label": "Pairing code",
|
||||||
|
"server": "Server",
|
||||||
|
"authorize_button": "Authorize",
|
||||||
|
"authorizing": "Authorizing...",
|
||||||
|
"scan_again": "Scan Again",
|
||||||
|
"done": "Done",
|
||||||
|
"success_title": "Authorization Sent",
|
||||||
|
"pairing_tv_connecting": "The TV is connecting to your account",
|
||||||
|
"error_title": "Authorization Failed",
|
||||||
|
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
|
||||||
|
"error_generic": "Something went wrong. Please try again.",
|
||||||
|
"error_permission_denied": "Camera permission is required to scan QR codes.",
|
||||||
|
"login_as": "Log in as {{username}}?",
|
||||||
|
"on_server": "on {{server}}",
|
||||||
|
"use_different_user": "Use a different user",
|
||||||
|
"open_settings": "Open Settings"
|
||||||
|
},
|
||||||
|
"pairing": {
|
||||||
|
"pair_with_phone": "Pair with Phone",
|
||||||
|
"pair_with_phone_title": "Login TV",
|
||||||
|
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
|
||||||
|
"waiting_for_phone": "Waiting for phone...",
|
||||||
|
"scan_with_phone": "Scan with the Streamyfin app on your phone",
|
||||||
|
"logging_in": "Logging in...",
|
||||||
|
"logging_in_description": "Connecting to your server"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
"error_title": "Virhe",
|
"error_title": "Virhe",
|
||||||
"login_title": "Kirjaudu sisään",
|
"login_title": "Kirjaudu sisään",
|
||||||
"login_to_title": "Kirjaudu sisään palveluun",
|
"login_to_title": "Kirjaudu sisään palveluun",
|
||||||
|
"select_user": "Select a user to log in",
|
||||||
|
"add_user_to_login": "Add a user to log in",
|
||||||
|
"add_user": "Add User",
|
||||||
"username_placeholder": "Käyttäjätunnus",
|
"username_placeholder": "Käyttäjätunnus",
|
||||||
"password_placeholder": "Salasana",
|
"password_placeholder": "Salasana",
|
||||||
"login_button": "Kirjaudu sisään",
|
"login_button": "Kirjaudu sisään",
|
||||||
@@ -42,7 +45,13 @@
|
|||||||
"accounts_count": "{{count}} accounts",
|
"accounts_count": "{{count}} accounts",
|
||||||
"select_account": "Select Account",
|
"select_account": "Select Account",
|
||||||
"add_account": "Add Account",
|
"add_account": "Add Account",
|
||||||
"remove_account_description": "This will remove the saved credentials for {{username}}."
|
"remove_account_description": "This will remove the saved credentials for {{username}}.",
|
||||||
|
"remove_server": "Remove Server",
|
||||||
|
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
|
||||||
|
"select_your_server": "Select Your Server",
|
||||||
|
"add_server_to_get_started": "Add a server to get started",
|
||||||
|
"add_server": "Add Server",
|
||||||
|
"change_server": "Change Server"
|
||||||
},
|
},
|
||||||
"save_account": {
|
"save_account": {
|
||||||
"title": "Save Account",
|
"title": "Save Account",
|
||||||
@@ -86,6 +95,7 @@
|
|||||||
"oops": "Ups!",
|
"oops": "Ups!",
|
||||||
"error_message": "Jotain meni pieleen.\nKirjaudu ulos ja takaisin sisään.",
|
"error_message": "Jotain meni pieleen.\nKirjaudu ulos ja takaisin sisään.",
|
||||||
"continue_watching": "Jatka katsomista",
|
"continue_watching": "Jatka katsomista",
|
||||||
|
"continue": "Continue",
|
||||||
"next_up": "Seuraavaksi",
|
"next_up": "Seuraavaksi",
|
||||||
"continue_and_next_up": "Continue & Next Up",
|
"continue_and_next_up": "Continue & Next Up",
|
||||||
"recently_added_in": "Äskettäin lisätty {{libraryName}}-kirjastoon",
|
"recently_added_in": "Äskettäin lisätty {{libraryName}}-kirjastoon",
|
||||||
@@ -109,6 +119,12 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"settings_title": "Asetukset",
|
"settings_title": "Asetukset",
|
||||||
"log_out_button": "Kirjaudu ulos",
|
"log_out_button": "Kirjaudu ulos",
|
||||||
|
"switch_user": {
|
||||||
|
"title": "Switch User",
|
||||||
|
"account": "Account",
|
||||||
|
"switch_user": "Switch User on This Server",
|
||||||
|
"current": "current"
|
||||||
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "Kategoriat"
|
"title": "Kategoriat"
|
||||||
},
|
},
|
||||||
@@ -121,7 +137,16 @@
|
|||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "Ulkoasu",
|
"title": "Ulkoasu",
|
||||||
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
||||||
"hide_remote_session_button": "Hide Remote Session Button"
|
"hide_remote_session_button": "Hide Remote Session Button",
|
||||||
|
"show_home_backdrop": "Dynamic Home Backdrop",
|
||||||
|
"show_hero_carousel": "Hero Carousel",
|
||||||
|
"show_series_poster_on_episode": "Show Series Poster on Episodes",
|
||||||
|
"theme_music": "Theme Music",
|
||||||
|
"display_size": "Display Size",
|
||||||
|
"display_size_small": "Small",
|
||||||
|
"display_size_default": "Default",
|
||||||
|
"display_size_large": "Large",
|
||||||
|
"display_size_extra_large": "Extra Large"
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"title": "Network",
|
"title": "Network",
|
||||||
@@ -174,6 +199,22 @@
|
|||||||
"rewind_length": "Taaksepäin hyppäämisen pituus",
|
"rewind_length": "Taaksepäin hyppäämisen pituus",
|
||||||
"seconds_unit": "s"
|
"seconds_unit": "s"
|
||||||
},
|
},
|
||||||
|
"buffer": {
|
||||||
|
"title": "Buffer Settings",
|
||||||
|
"cache_mode": "Cache Mode",
|
||||||
|
"cache_auto": "Auto",
|
||||||
|
"cache_yes": "Enabled",
|
||||||
|
"cache_no": "Disabled",
|
||||||
|
"buffer_duration": "Buffer Duration",
|
||||||
|
"max_cache_size": "Max Cache Size",
|
||||||
|
"max_backward_cache": "Max Backward Cache"
|
||||||
|
},
|
||||||
|
"vo_driver": {
|
||||||
|
"title": "Video Output",
|
||||||
|
"vo_mode": "VO Driver",
|
||||||
|
"gpu_next": "gpu-next (Recommended)",
|
||||||
|
"gpu": "gpu"
|
||||||
|
},
|
||||||
"gesture_controls": {
|
"gesture_controls": {
|
||||||
"gesture_controls_title": "Ele Ohjaus",
|
"gesture_controls_title": "Ele Ohjaus",
|
||||||
"horizontal_swipe_skip": "Ohita vaakatasossa pyyhkäisemällä",
|
"horizontal_swipe_skip": "Ohita vaakatasossa pyyhkäisemällä",
|
||||||
@@ -256,7 +297,23 @@
|
|||||||
"subtitle_font": "Subtitle Font",
|
"subtitle_font": "Subtitle Font",
|
||||||
"ksplayer_title": "KSPlayer Settings",
|
"ksplayer_title": "KSPlayer Settings",
|
||||||
"hardware_decode": "Hardware Decoding",
|
"hardware_decode": "Hardware Decoding",
|
||||||
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
|
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.",
|
||||||
|
"opensubtitles_title": "OpenSubtitles",
|
||||||
|
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
|
||||||
|
"opensubtitles_api_key": "API Key",
|
||||||
|
"opensubtitles_api_key_placeholder": "Enter API key...",
|
||||||
|
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
|
||||||
|
"mpv_subtitle_scale": "Subtitle Scale",
|
||||||
|
"mpv_subtitle_margin_y": "Vertical Margin",
|
||||||
|
"mpv_subtitle_align_x": "Horizontal Align",
|
||||||
|
"mpv_subtitle_align_y": "Vertical Align",
|
||||||
|
"align": {
|
||||||
|
"left": "Left",
|
||||||
|
"center": "Center",
|
||||||
|
"right": "Right",
|
||||||
|
"top": "Top",
|
||||||
|
"bottom": "Bottom"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"vlc_subtitles": {
|
"vlc_subtitles": {
|
||||||
"title": "VLC Subtitle Settings",
|
"title": "VLC Subtitle Settings",
|
||||||
@@ -406,7 +463,13 @@
|
|||||||
"music_cache_cleared": "Music cache cleared",
|
"music_cache_cleared": "Music cache cleared",
|
||||||
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
||||||
"downloaded_songs_size": "{{size}} downloaded",
|
"downloaded_songs_size": "{{size}} downloaded",
|
||||||
"downloaded_songs_deleted": "Downloaded songs deleted"
|
"downloaded_songs_deleted": "Downloaded songs deleted",
|
||||||
|
"clear_all_cache": "Clear All Cache",
|
||||||
|
"clear_all_cache_confirm": "Clear All Cache?",
|
||||||
|
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
|
||||||
|
"clear_all_cache_success": "Cache Cleared",
|
||||||
|
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
|
||||||
|
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "Esittely",
|
"title": "Esittely",
|
||||||
@@ -430,6 +493,21 @@
|
|||||||
"error_deleting_files": "Virhe tiedostojen poistamisessa",
|
"error_deleting_files": "Virhe tiedostojen poistamisessa",
|
||||||
"background_downloads_enabled": "Taustalataukset käytössä",
|
"background_downloads_enabled": "Taustalataukset käytössä",
|
||||||
"background_downloads_disabled": "Taustalataukset pois käytöstä"
|
"background_downloads_disabled": "Taustalataukset pois käytöstä"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"title": "Security",
|
||||||
|
"inactivity_timeout": {
|
||||||
|
"title": "Inactivity Timeout",
|
||||||
|
"description": "Auto logout after inactivity",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"1_minute": "1 minute",
|
||||||
|
"5_minutes": "5 minutes",
|
||||||
|
"15_minutes": "15 minutes",
|
||||||
|
"30_minutes": "30 minutes",
|
||||||
|
"1_hour": "1 hour",
|
||||||
|
"4_hours": "4 hours",
|
||||||
|
"24_hours": "24 hours"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -456,6 +534,7 @@
|
|||||||
"new_app_version_requires_re_download_description": "Uusi päivitys vaatii sisällön lataamista uudelleen. Poista kaikki ladattu sisältö ja yritä uudelleen.",
|
"new_app_version_requires_re_download_description": "Uusi päivitys vaatii sisällön lataamista uudelleen. Poista kaikki ladattu sisältö ja yritä uudelleen.",
|
||||||
"back": "Takaisin",
|
"back": "Takaisin",
|
||||||
"delete": "Poista",
|
"delete": "Poista",
|
||||||
|
"delete_download": "Delete Download",
|
||||||
"something_went_wrong": "Jotain meni pieleen",
|
"something_went_wrong": "Jotain meni pieleen",
|
||||||
"could_not_get_stream_url_from_jellyfin": "Ei voitu saada suoratoiston URL:ia Jellyfinilta",
|
"could_not_get_stream_url_from_jellyfin": "Ei voitu saada suoratoiston URL:ia Jellyfinilta",
|
||||||
"eta": "Arvio {{eta}}",
|
"eta": "Arvio {{eta}}",
|
||||||
@@ -492,22 +571,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"no_results": "No Results",
|
||||||
"select": "Valitse",
|
"select": "Valitse",
|
||||||
"no_trailer_available": "Perävaunua ei saatavilla",
|
"no_trailer_available": "Perävaunua ei saatavilla",
|
||||||
"video": "Video",
|
"video": "Video",
|
||||||
"audio": "Ääni",
|
"audio": "Ääni",
|
||||||
"subtitle": "Tekstitys",
|
"subtitle": "Tekstitys",
|
||||||
"play": "Toista",
|
"play": "Toista",
|
||||||
|
"mark_as_played": "Mark as Played",
|
||||||
|
"mark_as_not_played": "Mark as not Played",
|
||||||
"none": "Ei mitään",
|
"none": "Ei mitään",
|
||||||
"track": "Track",
|
"track": "Track",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
|
"stop": "Stop",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"verifying": "Verifying..."
|
"verifying": "Verifying...",
|
||||||
|
"login": "Login",
|
||||||
|
"refresh": "Refresh"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"search": "Haku...",
|
"search": "Haku...",
|
||||||
@@ -556,6 +641,7 @@
|
|||||||
"movies": "elokuvat",
|
"movies": "elokuvat",
|
||||||
"series": "sarjat",
|
"series": "sarjat",
|
||||||
"boxsets": "bokset",
|
"boxsets": "bokset",
|
||||||
|
"playlists": "Playlists",
|
||||||
"items": "kohteet"
|
"items": "kohteet"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
@@ -566,7 +652,8 @@
|
|||||||
"poster": "Juliste",
|
"poster": "Juliste",
|
||||||
"cover": "Kansi",
|
"cover": "Kansi",
|
||||||
"show_titles": "Näytä otsikot",
|
"show_titles": "Näytä otsikot",
|
||||||
"show_stats": "Näytä tilastot"
|
"show_stats": "Näytä tilastot",
|
||||||
|
"options_title": "Options"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"genres": "Genret",
|
"genres": "Genret",
|
||||||
@@ -574,7 +661,11 @@
|
|||||||
"sort_by": "Lajittele",
|
"sort_by": "Lajittele",
|
||||||
"filter_by": "Filter By",
|
"filter_by": "Filter By",
|
||||||
"sort_order": "Lajittelujärjestys",
|
"sort_order": "Lajittelujärjestys",
|
||||||
"tags": "Tunnisteet"
|
"tags": "Tunnisteet",
|
||||||
|
"all": "All",
|
||||||
|
"reset": "Reset",
|
||||||
|
"asc": "Ascending",
|
||||||
|
"desc": "Descending"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"favorites": {
|
"favorites": {
|
||||||
@@ -591,6 +682,8 @@
|
|||||||
"no_links": "Ei Linkkejä"
|
"no_links": "Ei Linkkejä"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
|
"live": "LIVE",
|
||||||
|
"mpv_player_title": "MPV Player",
|
||||||
"error": "Virhe",
|
"error": "Virhe",
|
||||||
"failed_to_get_stream_url": "Lähetyksen URL-osoitteen haku epäonnistui",
|
"failed_to_get_stream_url": "Lähetyksen URL-osoitteen haku epäonnistui",
|
||||||
"an_error_occured_while_playing_the_video": "Videon toiston yhteydessä tapahtui virhe. Tarkista lokit asetuksista.",
|
"an_error_occured_while_playing_the_video": "Videon toiston yhteydessä tapahtui virhe. Tarkista lokit asetuksista.",
|
||||||
@@ -608,7 +701,35 @@
|
|||||||
"downloaded_file_message": "Haluatko toistaa ladatun tiedoston?",
|
"downloaded_file_message": "Haluatko toistaa ladatun tiedoston?",
|
||||||
"downloaded_file_yes": "Kyllä",
|
"downloaded_file_yes": "Kyllä",
|
||||||
"downloaded_file_no": "Ei",
|
"downloaded_file_no": "Ei",
|
||||||
"downloaded_file_cancel": "Peruuta"
|
"downloaded_file_cancel": "Peruuta",
|
||||||
|
"swipe_down_settings": "Swipe down for settings",
|
||||||
|
"ends_at": "Ends at {{time}}",
|
||||||
|
"search_subtitles": "Search Subtitles",
|
||||||
|
"subtitle_tracks": "Tracks",
|
||||||
|
"subtitle_search": "Search & Download",
|
||||||
|
"download": "Download",
|
||||||
|
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
|
||||||
|
"using_jellyfin_server": "Using Jellyfin Server",
|
||||||
|
"language": "Language",
|
||||||
|
"results": "Results",
|
||||||
|
"searching": "Searching...",
|
||||||
|
"search_failed": "Search failed",
|
||||||
|
"no_subtitle_provider": "No subtitle provider configured on server",
|
||||||
|
"no_subtitles_found": "No subtitles found",
|
||||||
|
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
|
||||||
|
"settings": "Settings",
|
||||||
|
"skip_intro": "Skip Intro",
|
||||||
|
"skip_credits": "Skip Credits",
|
||||||
|
"stopPlayback": "Stop Playback",
|
||||||
|
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
|
||||||
|
"stopPlayingConfirm": "Are you sure you want to stop playback?",
|
||||||
|
"downloaded": "Downloaded"
|
||||||
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Seuraavaksi",
|
"next_up": "Seuraavaksi",
|
||||||
@@ -617,6 +738,11 @@
|
|||||||
"series": "Sarjat",
|
"series": "Sarjat",
|
||||||
"seasons": "Kaudet",
|
"seasons": "Kaudet",
|
||||||
"season": "Kausi",
|
"season": "Kausi",
|
||||||
|
"from_this_series": "From This Series",
|
||||||
|
"more_from_this_season": "More from this Season",
|
||||||
|
"view_series": "View Series",
|
||||||
|
"view_season": "View Season",
|
||||||
|
"select_season": "Select Season",
|
||||||
"no_episodes_for_this_season": "Ei jaksoja tälle kaudelle",
|
"no_episodes_for_this_season": "Ei jaksoja tälle kaudelle",
|
||||||
"overview": "Yleiskatsaus",
|
"overview": "Yleiskatsaus",
|
||||||
"more_with": "Enemmän {{name}} kanssa",
|
"more_with": "Enemmän {{name}} kanssa",
|
||||||
@@ -627,10 +753,21 @@
|
|||||||
"media_options": "Media-asetukset",
|
"media_options": "Media-asetukset",
|
||||||
"quality": "Laatu",
|
"quality": "Laatu",
|
||||||
"audio": "Ääni",
|
"audio": "Ääni",
|
||||||
"subtitles": "Tekstitys",
|
"subtitles": {
|
||||||
|
"label": "Subtitle",
|
||||||
|
"none": "None",
|
||||||
|
"tracks": "Tracks"
|
||||||
|
},
|
||||||
"show_more": "Näytä Lisää",
|
"show_more": "Näytä Lisää",
|
||||||
"show_less": "Näytä Vähemmän",
|
"show_less": "Näytä Vähemmän",
|
||||||
|
"left": "left",
|
||||||
|
"more_info": "More Info",
|
||||||
|
"director": "Director",
|
||||||
|
"cast": "Cast",
|
||||||
|
"technical_details": "Technical Details",
|
||||||
"appeared_in": "Esiintyy Sisään",
|
"appeared_in": "Esiintyy Sisään",
|
||||||
|
"movies": "Movies",
|
||||||
|
"shows": "Shows",
|
||||||
"could_not_load_item": "Kohdetta Ei Voitu Ladata",
|
"could_not_load_item": "Kohdetta Ei Voitu Ladata",
|
||||||
"none": "Ei mitään",
|
"none": "Ei mitään",
|
||||||
"download": {
|
"download": {
|
||||||
@@ -641,7 +778,13 @@
|
|||||||
"download_x_item": "Lataa {{item_count}} Kohteita",
|
"download_x_item": "Lataa {{item_count}} Kohteita",
|
||||||
"download_unwatched_only": "Vain Katsomattomat",
|
"download_unwatched_only": "Vain Katsomattomat",
|
||||||
"download_button": "Lataa"
|
"download_button": "Lataa"
|
||||||
}
|
},
|
||||||
|
"mark_played": "Mark as Watched",
|
||||||
|
"mark_unplayed": "Mark as Unwatched",
|
||||||
|
"resume_playback": "Resume Playback",
|
||||||
|
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
|
||||||
|
"play_from_start": "Play from Start",
|
||||||
|
"continue_from": "Continue from {{time}}"
|
||||||
},
|
},
|
||||||
"live_tv": {
|
"live_tv": {
|
||||||
"next": "Seuraava",
|
"next": "Seuraava",
|
||||||
@@ -652,7 +795,18 @@
|
|||||||
"movies": "Elokuvat",
|
"movies": "Elokuvat",
|
||||||
"sports": "Urheilu",
|
"sports": "Urheilu",
|
||||||
"for_kids": "Lapsille",
|
"for_kids": "Lapsille",
|
||||||
"news": "Uutiset"
|
"news": "Uutiset",
|
||||||
|
"page_of": "Page {{current}} of {{total}}",
|
||||||
|
"no_programs": "No programs available",
|
||||||
|
"no_channels": "No channels available",
|
||||||
|
"tabs": {
|
||||||
|
"programs": "Programs",
|
||||||
|
"guide": "Guide",
|
||||||
|
"channels": "Channels",
|
||||||
|
"recordings": "Recordings",
|
||||||
|
"schedule": "Schedule",
|
||||||
|
"series": "Series"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"jellyseerr": {
|
"jellyseerr": {
|
||||||
"confirm": "Vahvista",
|
"confirm": "Vahvista",
|
||||||
@@ -697,6 +851,12 @@
|
|||||||
"decline": "Hylkää",
|
"decline": "Hylkää",
|
||||||
"requested_by": "Käyttäjän {{user}} pyynnöstä",
|
"requested_by": "Käyttäjän {{user}} pyynnöstä",
|
||||||
"unknown_user": "Tuntematon käyttäjä",
|
"unknown_user": "Tuntematon käyttäjä",
|
||||||
|
"select": "Select",
|
||||||
|
"request_all": "Request All",
|
||||||
|
"request_seasons": "Request Seasons",
|
||||||
|
"select_seasons": "Select Seasons",
|
||||||
|
"request_selected": "Request Selected",
|
||||||
|
"n_selected": "{{count}} selected",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"jellyseer_does_not_meet_requirements": "Jellyseerr-palvelin ei täytä vähimmäisversiovaatimuksia! Päivitä vähintään versioon 2.0.0",
|
"jellyseer_does_not_meet_requirements": "Jellyseerr-palvelin ei täytä vähimmäisversiovaatimuksia! Päivitä vähintään versioon 2.0.0",
|
||||||
"jellyseerr_test_failed": "Jellyseerr-testi epäonnistui. Yritä uudelleen.",
|
"jellyseerr_test_failed": "Jellyseerr-testi epäonnistui. Yritä uudelleen.",
|
||||||
@@ -716,7 +876,8 @@
|
|||||||
"search": "Haku",
|
"search": "Haku",
|
||||||
"library": "Kirjasto",
|
"library": "Kirjasto",
|
||||||
"custom_links": "Mukautetut linkit",
|
"custom_links": "Mukautetut linkit",
|
||||||
"favorites": "Suosikit"
|
"favorites": "Suosikit",
|
||||||
|
"settings": "Settings"
|
||||||
},
|
},
|
||||||
"music": {
|
"music": {
|
||||||
"title": "Music",
|
"title": "Music",
|
||||||
@@ -841,5 +1002,36 @@
|
|||||||
"show": "This show",
|
"show": "This show",
|
||||||
"all": "All media (default)"
|
"all": "All media (default)"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"companion_login": {
|
||||||
|
"title": "Pair with TV",
|
||||||
|
"align_qr": "Align the QR code within the frame",
|
||||||
|
"enter_code_manually": "Enter code manually",
|
||||||
|
"pairing_enter_credentials": "Enter credentials for TV",
|
||||||
|
"pairing_code_label": "Pairing code",
|
||||||
|
"server": "Server",
|
||||||
|
"authorize_button": "Authorize",
|
||||||
|
"authorizing": "Authorizing...",
|
||||||
|
"scan_again": "Scan Again",
|
||||||
|
"done": "Done",
|
||||||
|
"success_title": "Authorization Sent",
|
||||||
|
"pairing_tv_connecting": "The TV is connecting to your account",
|
||||||
|
"error_title": "Authorization Failed",
|
||||||
|
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
|
||||||
|
"error_generic": "Something went wrong. Please try again.",
|
||||||
|
"error_permission_denied": "Camera permission is required to scan QR codes.",
|
||||||
|
"login_as": "Log in as {{username}}?",
|
||||||
|
"on_server": "on {{server}}",
|
||||||
|
"use_different_user": "Use a different user",
|
||||||
|
"open_settings": "Open Settings"
|
||||||
|
},
|
||||||
|
"pairing": {
|
||||||
|
"pair_with_phone": "Pair with Phone",
|
||||||
|
"pair_with_phone_title": "Login TV",
|
||||||
|
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
|
||||||
|
"waiting_for_phone": "Waiting for phone...",
|
||||||
|
"scan_with_phone": "Scan with the Streamyfin app on your phone",
|
||||||
|
"logging_in": "Logging in...",
|
||||||
|
"logging_in_description": "Connecting to your server"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
"error_title": "Erreur",
|
"error_title": "Erreur",
|
||||||
"login_title": "Se connecter",
|
"login_title": "Se connecter",
|
||||||
"login_to_title": "Se connecter à",
|
"login_to_title": "Se connecter à",
|
||||||
|
"select_user": "Select a user to log in",
|
||||||
|
"add_user_to_login": "Add a user to log in",
|
||||||
|
"add_user": "Add User",
|
||||||
"username_placeholder": "Nom d'utilisateur",
|
"username_placeholder": "Nom d'utilisateur",
|
||||||
"password_placeholder": "Mot de passe",
|
"password_placeholder": "Mot de passe",
|
||||||
"login_button": "Se connecter",
|
"login_button": "Se connecter",
|
||||||
@@ -42,7 +45,13 @@
|
|||||||
"accounts_count": "Comptes {{count}}",
|
"accounts_count": "Comptes {{count}}",
|
||||||
"select_account": "Sélectionnez un compte",
|
"select_account": "Sélectionnez un compte",
|
||||||
"add_account": "Ajouter un compte",
|
"add_account": "Ajouter un compte",
|
||||||
"remove_account_description": "Cela supprimera les identifiants enregistrés pour {{username}}."
|
"remove_account_description": "Cela supprimera les identifiants enregistrés pour {{username}}.",
|
||||||
|
"remove_server": "Remove Server",
|
||||||
|
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
|
||||||
|
"select_your_server": "Select Your Server",
|
||||||
|
"add_server_to_get_started": "Add a server to get started",
|
||||||
|
"add_server": "Add Server",
|
||||||
|
"change_server": "Change Server"
|
||||||
},
|
},
|
||||||
"save_account": {
|
"save_account": {
|
||||||
"title": "Sauvegarder le compte",
|
"title": "Sauvegarder le compte",
|
||||||
@@ -86,6 +95,7 @@
|
|||||||
"oops": "Oups !",
|
"oops": "Oups !",
|
||||||
"error_message": "Quelque chose s'est mal passé.\nVeuillez vous reconnecter à nouveau.",
|
"error_message": "Quelque chose s'est mal passé.\nVeuillez vous reconnecter à nouveau.",
|
||||||
"continue_watching": "Continuer à regarder",
|
"continue_watching": "Continuer à regarder",
|
||||||
|
"continue": "Continue",
|
||||||
"next_up": "À suivre",
|
"next_up": "À suivre",
|
||||||
"continue_and_next_up": "Continuer de regarder et à suivre",
|
"continue_and_next_up": "Continuer de regarder et à suivre",
|
||||||
"recently_added_in": "Ajoutés récemment dans {{libraryName}}",
|
"recently_added_in": "Ajoutés récemment dans {{libraryName}}",
|
||||||
@@ -109,6 +119,12 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"settings_title": "Paramètres",
|
"settings_title": "Paramètres",
|
||||||
"log_out_button": "Déconnexion",
|
"log_out_button": "Déconnexion",
|
||||||
|
"switch_user": {
|
||||||
|
"title": "Switch User",
|
||||||
|
"account": "Account",
|
||||||
|
"switch_user": "Switch User on This Server",
|
||||||
|
"current": "current"
|
||||||
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "Catégories"
|
"title": "Catégories"
|
||||||
},
|
},
|
||||||
@@ -121,7 +137,16 @@
|
|||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "Apparence",
|
"title": "Apparence",
|
||||||
"merge_next_up_continue_watching": "Fusionner, continuer à regarder et à suivre",
|
"merge_next_up_continue_watching": "Fusionner, continuer à regarder et à suivre",
|
||||||
"hide_remote_session_button": "Masquer le bouton de session distante"
|
"hide_remote_session_button": "Masquer le bouton de session distante",
|
||||||
|
"show_home_backdrop": "Dynamic Home Backdrop",
|
||||||
|
"show_hero_carousel": "Hero Carousel",
|
||||||
|
"show_series_poster_on_episode": "Show Series Poster on Episodes",
|
||||||
|
"theme_music": "Theme Music",
|
||||||
|
"display_size": "Display Size",
|
||||||
|
"display_size_small": "Small",
|
||||||
|
"display_size_default": "Default",
|
||||||
|
"display_size_large": "Large",
|
||||||
|
"display_size_extra_large": "Extra Large"
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"title": "Réseau",
|
"title": "Réseau",
|
||||||
@@ -174,6 +199,22 @@
|
|||||||
"rewind_length": "Durée de retour en arrière",
|
"rewind_length": "Durée de retour en arrière",
|
||||||
"seconds_unit": "s"
|
"seconds_unit": "s"
|
||||||
},
|
},
|
||||||
|
"buffer": {
|
||||||
|
"title": "Buffer Settings",
|
||||||
|
"cache_mode": "Cache Mode",
|
||||||
|
"cache_auto": "Auto",
|
||||||
|
"cache_yes": "Enabled",
|
||||||
|
"cache_no": "Disabled",
|
||||||
|
"buffer_duration": "Buffer Duration",
|
||||||
|
"max_cache_size": "Max Cache Size",
|
||||||
|
"max_backward_cache": "Max Backward Cache"
|
||||||
|
},
|
||||||
|
"vo_driver": {
|
||||||
|
"title": "Video Output",
|
||||||
|
"vo_mode": "VO Driver",
|
||||||
|
"gpu_next": "gpu-next (Recommended)",
|
||||||
|
"gpu": "gpu"
|
||||||
|
},
|
||||||
"gesture_controls": {
|
"gesture_controls": {
|
||||||
"gesture_controls_title": "Commandes gestuelles",
|
"gesture_controls_title": "Commandes gestuelles",
|
||||||
"horizontal_swipe_skip": "Glisser horizontalement pour passer",
|
"horizontal_swipe_skip": "Glisser horizontalement pour passer",
|
||||||
@@ -256,7 +297,23 @@
|
|||||||
"subtitle_font": "Police des sous-titres",
|
"subtitle_font": "Police des sous-titres",
|
||||||
"ksplayer_title": "Paramètres de KSPlayer",
|
"ksplayer_title": "Paramètres de KSPlayer",
|
||||||
"hardware_decode": "Décodage matériel",
|
"hardware_decode": "Décodage matériel",
|
||||||
"hardware_decode_description": "Utilisez l’accélération matérielle pour le décodage vidéo. Désactivez si vous rencontrez des problèmes de lecture."
|
"hardware_decode_description": "Utilisez l’accélération matérielle pour le décodage vidéo. Désactivez si vous rencontrez des problèmes de lecture.",
|
||||||
|
"opensubtitles_title": "OpenSubtitles",
|
||||||
|
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
|
||||||
|
"opensubtitles_api_key": "API Key",
|
||||||
|
"opensubtitles_api_key_placeholder": "Enter API key...",
|
||||||
|
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
|
||||||
|
"mpv_subtitle_scale": "Subtitle Scale",
|
||||||
|
"mpv_subtitle_margin_y": "Vertical Margin",
|
||||||
|
"mpv_subtitle_align_x": "Horizontal Align",
|
||||||
|
"mpv_subtitle_align_y": "Vertical Align",
|
||||||
|
"align": {
|
||||||
|
"left": "Left",
|
||||||
|
"center": "Center",
|
||||||
|
"right": "Right",
|
||||||
|
"top": "Top",
|
||||||
|
"bottom": "Bottom"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"vlc_subtitles": {
|
"vlc_subtitles": {
|
||||||
"title": "Paramètres des sous-titres VLC",
|
"title": "Paramètres des sous-titres VLC",
|
||||||
@@ -406,7 +463,13 @@
|
|||||||
"music_cache_cleared": "Cache de musique effacé",
|
"music_cache_cleared": "Cache de musique effacé",
|
||||||
"delete_all_downloaded_songs": "Supprimer toutes les musiques téléchargées",
|
"delete_all_downloaded_songs": "Supprimer toutes les musiques téléchargées",
|
||||||
"downloaded_songs_size": "{{size}} téléchargé",
|
"downloaded_songs_size": "{{size}} téléchargé",
|
||||||
"downloaded_songs_deleted": "Chansons téléchargées supprimées"
|
"downloaded_songs_deleted": "Chansons téléchargées supprimées",
|
||||||
|
"clear_all_cache": "Clear All Cache",
|
||||||
|
"clear_all_cache_confirm": "Clear All Cache?",
|
||||||
|
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
|
||||||
|
"clear_all_cache_success": "Cache Cleared",
|
||||||
|
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
|
||||||
|
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "Introduction",
|
"title": "Introduction",
|
||||||
@@ -430,6 +493,21 @@
|
|||||||
"error_deleting_files": "Erreur lors de la suppression des fichiers",
|
"error_deleting_files": "Erreur lors de la suppression des fichiers",
|
||||||
"background_downloads_enabled": "Téléchargements en arrière-plan activés",
|
"background_downloads_enabled": "Téléchargements en arrière-plan activés",
|
||||||
"background_downloads_disabled": "Téléchargements en arrière-plan désactivés"
|
"background_downloads_disabled": "Téléchargements en arrière-plan désactivés"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"title": "Security",
|
||||||
|
"inactivity_timeout": {
|
||||||
|
"title": "Inactivity Timeout",
|
||||||
|
"description": "Auto logout after inactivity",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"1_minute": "1 minute",
|
||||||
|
"5_minutes": "5 minutes",
|
||||||
|
"15_minutes": "15 minutes",
|
||||||
|
"30_minutes": "30 minutes",
|
||||||
|
"1_hour": "1 hour",
|
||||||
|
"4_hours": "4 hours",
|
||||||
|
"24_hours": "24 hours"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -456,6 +534,7 @@
|
|||||||
"new_app_version_requires_re_download_description": "La nouvelle mise à jour nécessite que le contenu soit téléchargé à nouveau. Veuillez supprimer tout le contenu téléchargé et réessayer.",
|
"new_app_version_requires_re_download_description": "La nouvelle mise à jour nécessite que le contenu soit téléchargé à nouveau. Veuillez supprimer tout le contenu téléchargé et réessayer.",
|
||||||
"back": "Retour",
|
"back": "Retour",
|
||||||
"delete": "Supprimer",
|
"delete": "Supprimer",
|
||||||
|
"delete_download": "Delete Download",
|
||||||
"something_went_wrong": "Quelque chose s'est mal passé",
|
"something_went_wrong": "Quelque chose s'est mal passé",
|
||||||
"could_not_get_stream_url_from_jellyfin": "Impossible d'obtenir l'URL du flux depuis Jellyfin",
|
"could_not_get_stream_url_from_jellyfin": "Impossible d'obtenir l'URL du flux depuis Jellyfin",
|
||||||
"eta": "ETA {{eta}}",
|
"eta": "ETA {{eta}}",
|
||||||
@@ -492,22 +571,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"no_results": "No Results",
|
||||||
"select": "Sélectionner",
|
"select": "Sélectionner",
|
||||||
"no_trailer_available": "Aucune bande-annonce disponible",
|
"no_trailer_available": "Aucune bande-annonce disponible",
|
||||||
"video": "Vidéo",
|
"video": "Vidéo",
|
||||||
"audio": "Audio",
|
"audio": "Audio",
|
||||||
"subtitle": "Sous-titres",
|
"subtitle": "Sous-titres",
|
||||||
"play": "Lecture",
|
"play": "Lecture",
|
||||||
|
"mark_as_played": "Mark as Played",
|
||||||
|
"mark_as_not_played": "Mark as not Played",
|
||||||
"none": "Aucun",
|
"none": "Aucun",
|
||||||
"track": "Suivre",
|
"track": "Suivre",
|
||||||
"cancel": "Annuler",
|
"cancel": "Annuler",
|
||||||
|
"stop": "Stop",
|
||||||
"delete": "Supprimer",
|
"delete": "Supprimer",
|
||||||
"ok": "Ok",
|
"ok": "Ok",
|
||||||
"remove": "Retirer",
|
"remove": "Retirer",
|
||||||
"next": "Suivant",
|
"next": "Suivant",
|
||||||
"back": "Précédent",
|
"back": "Précédent",
|
||||||
"continue": "Continuer",
|
"continue": "Continuer",
|
||||||
"verifying": "Vérification..."
|
"verifying": "Vérification...",
|
||||||
|
"login": "Login",
|
||||||
|
"refresh": "Refresh"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"search": "Rechercher...",
|
"search": "Rechercher...",
|
||||||
@@ -556,6 +641,7 @@
|
|||||||
"movies": "Films",
|
"movies": "Films",
|
||||||
"series": "Séries",
|
"series": "Séries",
|
||||||
"boxsets": "Coffrets ",
|
"boxsets": "Coffrets ",
|
||||||
|
"playlists": "Playlists",
|
||||||
"items": "Médias"
|
"items": "Médias"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
@@ -566,7 +652,8 @@
|
|||||||
"poster": "Affiche",
|
"poster": "Affiche",
|
||||||
"cover": "Couverture",
|
"cover": "Couverture",
|
||||||
"show_titles": "Afficher les titres",
|
"show_titles": "Afficher les titres",
|
||||||
"show_stats": "Afficher les statistiques"
|
"show_stats": "Afficher les statistiques",
|
||||||
|
"options_title": "Options"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"genres": "Genres",
|
"genres": "Genres",
|
||||||
@@ -574,7 +661,11 @@
|
|||||||
"sort_by": "Trier par",
|
"sort_by": "Trier par",
|
||||||
"filter_by": "Filtrer par",
|
"filter_by": "Filtrer par",
|
||||||
"sort_order": "Ordre de tri",
|
"sort_order": "Ordre de tri",
|
||||||
"tags": "Tags"
|
"tags": "Tags",
|
||||||
|
"all": "All",
|
||||||
|
"reset": "Reset",
|
||||||
|
"asc": "Ascending",
|
||||||
|
"desc": "Descending"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"favorites": {
|
"favorites": {
|
||||||
@@ -591,6 +682,8 @@
|
|||||||
"no_links": "Aucuns liens"
|
"no_links": "Aucuns liens"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
|
"live": "LIVE",
|
||||||
|
"mpv_player_title": "MPV Player",
|
||||||
"error": "Erreur",
|
"error": "Erreur",
|
||||||
"failed_to_get_stream_url": "Échec de l'obtention de l'URL du flux",
|
"failed_to_get_stream_url": "Échec de l'obtention de l'URL du flux",
|
||||||
"an_error_occured_while_playing_the_video": "Une erreur s’est produite lors de la lecture de la vidéo. Vérifiez les journaux dans les paramètres.",
|
"an_error_occured_while_playing_the_video": "Une erreur s’est produite lors de la lecture de la vidéo. Vérifiez les journaux dans les paramètres.",
|
||||||
@@ -608,7 +701,35 @@
|
|||||||
"downloaded_file_message": "Voulez-vous lire le fichier téléchargé ?",
|
"downloaded_file_message": "Voulez-vous lire le fichier téléchargé ?",
|
||||||
"downloaded_file_yes": "Oui",
|
"downloaded_file_yes": "Oui",
|
||||||
"downloaded_file_no": "Non",
|
"downloaded_file_no": "Non",
|
||||||
"downloaded_file_cancel": "Annuler"
|
"downloaded_file_cancel": "Annuler",
|
||||||
|
"swipe_down_settings": "Swipe down for settings",
|
||||||
|
"ends_at": "Ends at {{time}}",
|
||||||
|
"search_subtitles": "Search Subtitles",
|
||||||
|
"subtitle_tracks": "Tracks",
|
||||||
|
"subtitle_search": "Search & Download",
|
||||||
|
"download": "Download",
|
||||||
|
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
|
||||||
|
"using_jellyfin_server": "Using Jellyfin Server",
|
||||||
|
"language": "Language",
|
||||||
|
"results": "Results",
|
||||||
|
"searching": "Searching...",
|
||||||
|
"search_failed": "Search failed",
|
||||||
|
"no_subtitle_provider": "No subtitle provider configured on server",
|
||||||
|
"no_subtitles_found": "No subtitles found",
|
||||||
|
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
|
||||||
|
"settings": "Settings",
|
||||||
|
"skip_intro": "Skip Intro",
|
||||||
|
"skip_credits": "Skip Credits",
|
||||||
|
"stopPlayback": "Stop Playback",
|
||||||
|
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
|
||||||
|
"stopPlayingConfirm": "Are you sure you want to stop playback?",
|
||||||
|
"downloaded": "Downloaded"
|
||||||
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "À suivre",
|
"next_up": "À suivre",
|
||||||
@@ -617,6 +738,11 @@
|
|||||||
"series": "Séries",
|
"series": "Séries",
|
||||||
"seasons": "Saisons",
|
"seasons": "Saisons",
|
||||||
"season": "Saison",
|
"season": "Saison",
|
||||||
|
"from_this_series": "From This Series",
|
||||||
|
"more_from_this_season": "More from this Season",
|
||||||
|
"view_series": "View Series",
|
||||||
|
"view_season": "View Season",
|
||||||
|
"select_season": "Select Season",
|
||||||
"no_episodes_for_this_season": "Aucun épisode pour cette saison",
|
"no_episodes_for_this_season": "Aucun épisode pour cette saison",
|
||||||
"overview": "Aperçu",
|
"overview": "Aperçu",
|
||||||
"more_with": "Plus avec {{name}}",
|
"more_with": "Plus avec {{name}}",
|
||||||
@@ -627,10 +753,21 @@
|
|||||||
"media_options": "Options média",
|
"media_options": "Options média",
|
||||||
"quality": "Qualité",
|
"quality": "Qualité",
|
||||||
"audio": "Audio",
|
"audio": "Audio",
|
||||||
"subtitles": "Sous-titres",
|
"subtitles": {
|
||||||
|
"label": "Subtitle",
|
||||||
|
"none": "None",
|
||||||
|
"tracks": "Tracks"
|
||||||
|
},
|
||||||
"show_more": "Afficher plus",
|
"show_more": "Afficher plus",
|
||||||
"show_less": "Afficher moins",
|
"show_less": "Afficher moins",
|
||||||
|
"left": "left",
|
||||||
|
"more_info": "More Info",
|
||||||
|
"director": "Director",
|
||||||
|
"cast": "Cast",
|
||||||
|
"technical_details": "Technical Details",
|
||||||
"appeared_in": "Apparu dans",
|
"appeared_in": "Apparu dans",
|
||||||
|
"movies": "Movies",
|
||||||
|
"shows": "Shows",
|
||||||
"could_not_load_item": "Impossible de charger le média",
|
"could_not_load_item": "Impossible de charger le média",
|
||||||
"none": "Aucun",
|
"none": "Aucun",
|
||||||
"download": {
|
"download": {
|
||||||
@@ -641,7 +778,13 @@
|
|||||||
"download_x_item": "Télécharger {{item_count}} médias",
|
"download_x_item": "Télécharger {{item_count}} médias",
|
||||||
"download_unwatched_only": "Non visionné uniquement",
|
"download_unwatched_only": "Non visionné uniquement",
|
||||||
"download_button": "Télécharger"
|
"download_button": "Télécharger"
|
||||||
}
|
},
|
||||||
|
"mark_played": "Mark as Watched",
|
||||||
|
"mark_unplayed": "Mark as Unwatched",
|
||||||
|
"resume_playback": "Resume Playback",
|
||||||
|
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
|
||||||
|
"play_from_start": "Play from Start",
|
||||||
|
"continue_from": "Continue from {{time}}"
|
||||||
},
|
},
|
||||||
"live_tv": {
|
"live_tv": {
|
||||||
"next": "Suivant",
|
"next": "Suivant",
|
||||||
@@ -652,7 +795,18 @@
|
|||||||
"movies": "Films",
|
"movies": "Films",
|
||||||
"sports": "Sports",
|
"sports": "Sports",
|
||||||
"for_kids": "Pour enfants",
|
"for_kids": "Pour enfants",
|
||||||
"news": "Actualités"
|
"news": "Actualités",
|
||||||
|
"page_of": "Page {{current}} of {{total}}",
|
||||||
|
"no_programs": "No programs available",
|
||||||
|
"no_channels": "No channels available",
|
||||||
|
"tabs": {
|
||||||
|
"programs": "Programs",
|
||||||
|
"guide": "Guide",
|
||||||
|
"channels": "Channels",
|
||||||
|
"recordings": "Recordings",
|
||||||
|
"schedule": "Schedule",
|
||||||
|
"series": "Series"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"jellyseerr": {
|
"jellyseerr": {
|
||||||
"confirm": "Confirmer",
|
"confirm": "Confirmer",
|
||||||
@@ -697,6 +851,12 @@
|
|||||||
"decline": "Refuser",
|
"decline": "Refuser",
|
||||||
"requested_by": "Demandé par {{user}}",
|
"requested_by": "Demandé par {{user}}",
|
||||||
"unknown_user": "Utilisateur inconnu",
|
"unknown_user": "Utilisateur inconnu",
|
||||||
|
"select": "Select",
|
||||||
|
"request_all": "Request All",
|
||||||
|
"request_seasons": "Request Seasons",
|
||||||
|
"select_seasons": "Select Seasons",
|
||||||
|
"request_selected": "Request Selected",
|
||||||
|
"n_selected": "{{count}} selected",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"jellyseer_does_not_meet_requirements": "Seerr ne répond pas aux exigences ! Veuillez mettre à jour au moins vers la version 2.0.0.",
|
"jellyseer_does_not_meet_requirements": "Seerr ne répond pas aux exigences ! Veuillez mettre à jour au moins vers la version 2.0.0.",
|
||||||
"jellyseerr_test_failed": "Le test Seerr a échoué. Veuillez réessayer.",
|
"jellyseerr_test_failed": "Le test Seerr a échoué. Veuillez réessayer.",
|
||||||
@@ -716,7 +876,8 @@
|
|||||||
"search": "Recherche",
|
"search": "Recherche",
|
||||||
"library": "Bibliothèque",
|
"library": "Bibliothèque",
|
||||||
"custom_links": "Liens personnalisés",
|
"custom_links": "Liens personnalisés",
|
||||||
"favorites": "Favoris"
|
"favorites": "Favoris",
|
||||||
|
"settings": "Settings"
|
||||||
},
|
},
|
||||||
"music": {
|
"music": {
|
||||||
"title": "Musique",
|
"title": "Musique",
|
||||||
@@ -788,8 +949,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"watchlists": {
|
"watchlists": {
|
||||||
"title": "Watchlists",
|
"title": "Listes de lecture",
|
||||||
"my_watchlists": "My Watchlists",
|
"my_watchlists": "Mes listes de lecture",
|
||||||
"public_watchlists": "Watchlist publique",
|
"public_watchlists": "Watchlist publique",
|
||||||
"create_title": "Créer une Watchlist",
|
"create_title": "Créer une Watchlist",
|
||||||
"edit_title": "Modifier la Watchlist",
|
"edit_title": "Modifier la Watchlist",
|
||||||
@@ -802,7 +963,7 @@
|
|||||||
"name_placeholder": "Entrer le nom de la playlist",
|
"name_placeholder": "Entrer le nom de la playlist",
|
||||||
"description_label": "Description",
|
"description_label": "Description",
|
||||||
"description_placeholder": "Entrez la description (facultatif)",
|
"description_placeholder": "Entrez la description (facultatif)",
|
||||||
"is_public_label": "Public Watchlist",
|
"is_public_label": "Liste de lecture Publique",
|
||||||
"is_public_description": "Autoriser d'autres personnes à voir cette liste de suivi",
|
"is_public_description": "Autoriser d'autres personnes à voir cette liste de suivi",
|
||||||
"allowed_type_label": "Type de contenu",
|
"allowed_type_label": "Type de contenu",
|
||||||
"sort_order_label": "Ordre de tri par défaut",
|
"sort_order_label": "Ordre de tri par défaut",
|
||||||
@@ -841,5 +1002,36 @@
|
|||||||
"show": "Cette série",
|
"show": "Cette série",
|
||||||
"all": "Tous les médias (par défaut)"
|
"all": "Tous les médias (par défaut)"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"companion_login": {
|
||||||
|
"title": "Pair with TV",
|
||||||
|
"align_qr": "Align the QR code within the frame",
|
||||||
|
"enter_code_manually": "Enter code manually",
|
||||||
|
"pairing_enter_credentials": "Enter credentials for TV",
|
||||||
|
"pairing_code_label": "Pairing code",
|
||||||
|
"server": "Server",
|
||||||
|
"authorize_button": "Authorize",
|
||||||
|
"authorizing": "Authorizing...",
|
||||||
|
"scan_again": "Scan Again",
|
||||||
|
"done": "Done",
|
||||||
|
"success_title": "Authorization Sent",
|
||||||
|
"pairing_tv_connecting": "The TV is connecting to your account",
|
||||||
|
"error_title": "Authorization Failed",
|
||||||
|
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
|
||||||
|
"error_generic": "Something went wrong. Please try again.",
|
||||||
|
"error_permission_denied": "Camera permission is required to scan QR codes.",
|
||||||
|
"login_as": "Log in as {{username}}?",
|
||||||
|
"on_server": "on {{server}}",
|
||||||
|
"use_different_user": "Use a different user",
|
||||||
|
"open_settings": "Open Settings"
|
||||||
|
},
|
||||||
|
"pairing": {
|
||||||
|
"pair_with_phone": "Pair with Phone",
|
||||||
|
"pair_with_phone_title": "Login TV",
|
||||||
|
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
|
||||||
|
"waiting_for_phone": "Waiting for phone...",
|
||||||
|
"scan_with_phone": "Scan with the Streamyfin app on your phone",
|
||||||
|
"logging_in": "Logging in...",
|
||||||
|
"logging_in_description": "Connecting to your server"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
"error_title": "שגיאה",
|
"error_title": "שגיאה",
|
||||||
"login_title": "התחבר",
|
"login_title": "התחבר",
|
||||||
"login_to_title": "התחבר אל",
|
"login_to_title": "התחבר אל",
|
||||||
|
"select_user": "Select a user to log in",
|
||||||
|
"add_user_to_login": "Add a user to log in",
|
||||||
|
"add_user": "Add User",
|
||||||
"username_placeholder": "שם משתמש",
|
"username_placeholder": "שם משתמש",
|
||||||
"password_placeholder": "סיסמה",
|
"password_placeholder": "סיסמה",
|
||||||
"login_button": "התחבר",
|
"login_button": "התחבר",
|
||||||
@@ -39,10 +42,16 @@
|
|||||||
"please_login_again": "Your saved session has expired. Please log in again.",
|
"please_login_again": "Your saved session has expired. Please log in again.",
|
||||||
"remove_saved_login": "Remove Saved Login",
|
"remove_saved_login": "Remove Saved Login",
|
||||||
"remove_saved_login_description": "This will remove your saved credentials for this server. You'll need to enter your username and password again next time.",
|
"remove_saved_login_description": "This will remove your saved credentials for this server. You'll need to enter your username and password again next time.",
|
||||||
"accounts_count": "{{count}} accounts",
|
"accounts_count": "{{count}} חשבונות",
|
||||||
"select_account": "Select Account",
|
"select_account": "Select Account",
|
||||||
"add_account": "Add Account",
|
"add_account": "Add Account",
|
||||||
"remove_account_description": "This will remove the saved credentials for {{username}}."
|
"remove_account_description": "This will remove the saved credentials for {{username}}.",
|
||||||
|
"remove_server": "Remove Server",
|
||||||
|
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
|
||||||
|
"select_your_server": "Select Your Server",
|
||||||
|
"add_server_to_get_started": "Add a server to get started",
|
||||||
|
"add_server": "Add Server",
|
||||||
|
"change_server": "Change Server"
|
||||||
},
|
},
|
||||||
"save_account": {
|
"save_account": {
|
||||||
"title": "Save Account",
|
"title": "Save Account",
|
||||||
@@ -86,6 +95,7 @@
|
|||||||
"oops": "אופס!",
|
"oops": "אופס!",
|
||||||
"error_message": "קרתה תקלה. אנא התנתק והתחבר מחדש.",
|
"error_message": "קרתה תקלה. אנא התנתק והתחבר מחדש.",
|
||||||
"continue_watching": "המשך לצפות",
|
"continue_watching": "המשך לצפות",
|
||||||
|
"continue": "Continue",
|
||||||
"next_up": "הבא בתור",
|
"next_up": "הבא בתור",
|
||||||
"continue_and_next_up": "Continue & Next Up",
|
"continue_and_next_up": "Continue & Next Up",
|
||||||
"recently_added_in": "התווסף לאחרונה ב-{{libraryName}}",
|
"recently_added_in": "התווסף לאחרונה ב-{{libraryName}}",
|
||||||
@@ -109,19 +119,34 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"settings_title": "הגדרות",
|
"settings_title": "הגדרות",
|
||||||
"log_out_button": "התנתק",
|
"log_out_button": "התנתק",
|
||||||
|
"switch_user": {
|
||||||
|
"title": "Switch User",
|
||||||
|
"account": "Account",
|
||||||
|
"switch_user": "Switch User on This Server",
|
||||||
|
"current": "current"
|
||||||
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "Categories"
|
"title": "קטגוריות"
|
||||||
},
|
},
|
||||||
"playback_controls": {
|
"playback_controls": {
|
||||||
"title": "Playback & Controls"
|
"title": "Playback & Controls"
|
||||||
},
|
},
|
||||||
"audio_subtitles": {
|
"audio_subtitles": {
|
||||||
"title": "Audio & Subtitles"
|
"title": "שמע וכתוביות"
|
||||||
},
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "Appearance",
|
"title": "מראה",
|
||||||
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
||||||
"hide_remote_session_button": "Hide Remote Session Button"
|
"hide_remote_session_button": "Hide Remote Session Button",
|
||||||
|
"show_home_backdrop": "Dynamic Home Backdrop",
|
||||||
|
"show_hero_carousel": "Hero Carousel",
|
||||||
|
"show_series_poster_on_episode": "Show Series Poster on Episodes",
|
||||||
|
"theme_music": "Theme Music",
|
||||||
|
"display_size": "Display Size",
|
||||||
|
"display_size_small": "Small",
|
||||||
|
"display_size_default": "Default",
|
||||||
|
"display_size_large": "Large",
|
||||||
|
"display_size_extra_large": "Extra Large"
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"title": "Network",
|
"title": "Network",
|
||||||
@@ -174,6 +199,22 @@
|
|||||||
"rewind_length": "אורך הזזה אחורה",
|
"rewind_length": "אורך הזזה אחורה",
|
||||||
"seconds_unit": "שנ'"
|
"seconds_unit": "שנ'"
|
||||||
},
|
},
|
||||||
|
"buffer": {
|
||||||
|
"title": "Buffer Settings",
|
||||||
|
"cache_mode": "Cache Mode",
|
||||||
|
"cache_auto": "Auto",
|
||||||
|
"cache_yes": "Enabled",
|
||||||
|
"cache_no": "Disabled",
|
||||||
|
"buffer_duration": "Buffer Duration",
|
||||||
|
"max_cache_size": "Max Cache Size",
|
||||||
|
"max_backward_cache": "Max Backward Cache"
|
||||||
|
},
|
||||||
|
"vo_driver": {
|
||||||
|
"title": "Video Output",
|
||||||
|
"vo_mode": "VO Driver",
|
||||||
|
"gpu_next": "gpu-next (Recommended)",
|
||||||
|
"gpu": "gpu"
|
||||||
|
},
|
||||||
"gesture_controls": {
|
"gesture_controls": {
|
||||||
"gesture_controls_title": "פקדי מחוות",
|
"gesture_controls_title": "פקדי מחוות",
|
||||||
"horizontal_swipe_skip": "החלקה אופקית לדילוג",
|
"horizontal_swipe_skip": "החלקה אופקית לדילוג",
|
||||||
@@ -188,7 +229,7 @@
|
|||||||
"hide_brightness_slider_description": "Hide the brightness slider in the video player"
|
"hide_brightness_slider_description": "Hide the brightness slider in the video player"
|
||||||
},
|
},
|
||||||
"audio": {
|
"audio": {
|
||||||
"audio_title": "אודיו",
|
"audio_title": "שמע",
|
||||||
"set_audio_track": "בחר רצועת שמע מהפריט הקודם",
|
"set_audio_track": "בחר רצועת שמע מהפריט הקודם",
|
||||||
"audio_language": "שפת שמע",
|
"audio_language": "שפת שמע",
|
||||||
"audio_hint": "בחר שפת שמע אוטומטית.",
|
"audio_hint": "בחר שפת שמע אוטומטית.",
|
||||||
@@ -256,7 +297,23 @@
|
|||||||
"subtitle_font": "Subtitle Font",
|
"subtitle_font": "Subtitle Font",
|
||||||
"ksplayer_title": "KSPlayer Settings",
|
"ksplayer_title": "KSPlayer Settings",
|
||||||
"hardware_decode": "Hardware Decoding",
|
"hardware_decode": "Hardware Decoding",
|
||||||
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
|
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.",
|
||||||
|
"opensubtitles_title": "OpenSubtitles",
|
||||||
|
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
|
||||||
|
"opensubtitles_api_key": "API Key",
|
||||||
|
"opensubtitles_api_key_placeholder": "Enter API key...",
|
||||||
|
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
|
||||||
|
"mpv_subtitle_scale": "Subtitle Scale",
|
||||||
|
"mpv_subtitle_margin_y": "Vertical Margin",
|
||||||
|
"mpv_subtitle_align_x": "Horizontal Align",
|
||||||
|
"mpv_subtitle_align_y": "Vertical Align",
|
||||||
|
"align": {
|
||||||
|
"left": "Left",
|
||||||
|
"center": "Center",
|
||||||
|
"right": "Right",
|
||||||
|
"top": "Top",
|
||||||
|
"bottom": "Bottom"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"vlc_subtitles": {
|
"vlc_subtitles": {
|
||||||
"title": "VLC Subtitle Settings",
|
"title": "VLC Subtitle Settings",
|
||||||
@@ -271,8 +328,8 @@
|
|||||||
"margin": "Bottom Margin"
|
"margin": "Bottom Margin"
|
||||||
},
|
},
|
||||||
"video_player": {
|
"video_player": {
|
||||||
"title": "Video Player",
|
"title": "נגן וידאו",
|
||||||
"video_player": "Video Player",
|
"video_player": "נגן וידאו",
|
||||||
"video_player_description": "Choose which video player to use on iOS.",
|
"video_player_description": "Choose which video player to use on iOS.",
|
||||||
"ksplayer": "KSPlayer",
|
"ksplayer": "KSPlayer",
|
||||||
"vlc": "VLC"
|
"vlc": "VLC"
|
||||||
@@ -314,7 +371,7 @@
|
|||||||
"downloads_title": "הורדות"
|
"downloads_title": "הורדות"
|
||||||
},
|
},
|
||||||
"music": {
|
"music": {
|
||||||
"title": "Music",
|
"title": "מוזיקה",
|
||||||
"playback_title": "Playback",
|
"playback_title": "Playback",
|
||||||
"playback_description": "Configure how music is played.",
|
"playback_description": "Configure how music is played.",
|
||||||
"prefer_downloaded": "Prefer Downloaded Songs",
|
"prefer_downloaded": "Prefer Downloaded Songs",
|
||||||
@@ -406,10 +463,16 @@
|
|||||||
"music_cache_cleared": "Music cache cleared",
|
"music_cache_cleared": "Music cache cleared",
|
||||||
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
||||||
"downloaded_songs_size": "{{size}} downloaded",
|
"downloaded_songs_size": "{{size}} downloaded",
|
||||||
"downloaded_songs_deleted": "Downloaded songs deleted"
|
"downloaded_songs_deleted": "Downloaded songs deleted",
|
||||||
|
"clear_all_cache": "Clear All Cache",
|
||||||
|
"clear_all_cache_confirm": "Clear All Cache?",
|
||||||
|
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
|
||||||
|
"clear_all_cache_success": "Cache Cleared",
|
||||||
|
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
|
||||||
|
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "Intro",
|
"title": "הקדמה",
|
||||||
"show_intro": "הצג פתיח",
|
"show_intro": "הצג פתיח",
|
||||||
"reset_intro": "אפס פתיח"
|
"reset_intro": "אפס פתיח"
|
||||||
},
|
},
|
||||||
@@ -430,6 +493,21 @@
|
|||||||
"error_deleting_files": "שגיאה במחיקת קבצים",
|
"error_deleting_files": "שגיאה במחיקת קבצים",
|
||||||
"background_downloads_enabled": "הורדה ברקע מופעלת",
|
"background_downloads_enabled": "הורדה ברקע מופעלת",
|
||||||
"background_downloads_disabled": "הורדה ברקע כבויה"
|
"background_downloads_disabled": "הורדה ברקע כבויה"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"title": "Security",
|
||||||
|
"inactivity_timeout": {
|
||||||
|
"title": "Inactivity Timeout",
|
||||||
|
"description": "Auto logout after inactivity",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"1_minute": "1 minute",
|
||||||
|
"5_minutes": "5 minutes",
|
||||||
|
"15_minutes": "15 minutes",
|
||||||
|
"30_minutes": "30 minutes",
|
||||||
|
"1_hour": "1 hour",
|
||||||
|
"4_hours": "4 hours",
|
||||||
|
"24_hours": "24 hours"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -456,6 +534,7 @@
|
|||||||
"new_app_version_requires_re_download_description": "הגרסה החדשה דורשת לתוכן לרדת מחדש. אנא מחק את כל התוכן שכבר הורדת ונסה שוב.",
|
"new_app_version_requires_re_download_description": "הגרסה החדשה דורשת לתוכן לרדת מחדש. אנא מחק את כל התוכן שכבר הורדת ונסה שוב.",
|
||||||
"back": "חזרה",
|
"back": "חזרה",
|
||||||
"delete": "מחק",
|
"delete": "מחק",
|
||||||
|
"delete_download": "Delete Download",
|
||||||
"something_went_wrong": "משהו השתבש",
|
"something_went_wrong": "משהו השתבש",
|
||||||
"could_not_get_stream_url_from_jellyfin": "לא היה ניתן להגיע לקישור הזרם מהשרת Jellyfin",
|
"could_not_get_stream_url_from_jellyfin": "לא היה ניתן להגיע לקישור הזרם מהשרת Jellyfin",
|
||||||
"eta": "זמן משוער {{eta}}",
|
"eta": "זמן משוער {{eta}}",
|
||||||
@@ -492,22 +571,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"no_results": "No Results",
|
||||||
"select": "בחר",
|
"select": "בחר",
|
||||||
"no_trailer_available": "אין טריילר זמין",
|
"no_trailer_available": "אין טריילר זמין",
|
||||||
"video": "וידאו",
|
"video": "וידאו",
|
||||||
"audio": "אודיו",
|
"audio": "שמע",
|
||||||
"subtitle": "כתובית",
|
"subtitle": "כתובית",
|
||||||
"play": "נגן",
|
"play": "נגן",
|
||||||
|
"mark_as_played": "Mark as Played",
|
||||||
|
"mark_as_not_played": "Mark as not Played",
|
||||||
"none": "ללא",
|
"none": "ללא",
|
||||||
"track": "Track",
|
"track": "Track",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
|
"stop": "Stop",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"verifying": "Verifying..."
|
"verifying": "Verifying...",
|
||||||
|
"login": "Login",
|
||||||
|
"refresh": "Refresh"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"search": "חפש...",
|
"search": "חפש...",
|
||||||
@@ -521,9 +606,9 @@
|
|||||||
"episodes": "פרקים",
|
"episodes": "פרקים",
|
||||||
"collections": "אוספים",
|
"collections": "אוספים",
|
||||||
"actors": "שחקנים",
|
"actors": "שחקנים",
|
||||||
"artists": "Artists",
|
"artists": "אומנים",
|
||||||
"albums": "Albums",
|
"albums": "אלבומים",
|
||||||
"songs": "Songs",
|
"songs": "שירים",
|
||||||
"playlists": "Playlists",
|
"playlists": "Playlists",
|
||||||
"request_movies": "סרטים מבוקשים",
|
"request_movies": "סרטים מבוקשים",
|
||||||
"request_series": "סדרות מבוקשים",
|
"request_series": "סדרות מבוקשים",
|
||||||
@@ -556,6 +641,7 @@
|
|||||||
"movies": "סרטים",
|
"movies": "סרטים",
|
||||||
"series": "סדרות",
|
"series": "סדרות",
|
||||||
"boxsets": "אוסף",
|
"boxsets": "אוסף",
|
||||||
|
"playlists": "Playlists",
|
||||||
"items": "פריטים"
|
"items": "פריטים"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
@@ -566,7 +652,8 @@
|
|||||||
"poster": "פוסטר",
|
"poster": "פוסטר",
|
||||||
"cover": "עטיפה",
|
"cover": "עטיפה",
|
||||||
"show_titles": "הצג כותרות",
|
"show_titles": "הצג כותרות",
|
||||||
"show_stats": "הצג סטטיסטיקה"
|
"show_stats": "הצג סטטיסטיקה",
|
||||||
|
"options_title": "Options"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"genres": "סגנונות",
|
"genres": "סגנונות",
|
||||||
@@ -574,7 +661,11 @@
|
|||||||
"sort_by": "מיין לפי",
|
"sort_by": "מיין לפי",
|
||||||
"filter_by": "Filter By",
|
"filter_by": "Filter By",
|
||||||
"sort_order": "סדר מיון",
|
"sort_order": "סדר מיון",
|
||||||
"tags": "תגים"
|
"tags": "תגים",
|
||||||
|
"all": "All",
|
||||||
|
"reset": "Reset",
|
||||||
|
"asc": "Ascending",
|
||||||
|
"desc": "Descending"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"favorites": {
|
"favorites": {
|
||||||
@@ -591,6 +682,8 @@
|
|||||||
"no_links": "אין קישורים"
|
"no_links": "אין קישורים"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
|
"live": "LIVE",
|
||||||
|
"mpv_player_title": "MPV Player",
|
||||||
"error": "שגיאה",
|
"error": "שגיאה",
|
||||||
"failed_to_get_stream_url": "נכשל בהשגת קישור הזרם",
|
"failed_to_get_stream_url": "נכשל בהשגת קישור הזרם",
|
||||||
"an_error_occured_while_playing_the_video": "קרתה תקלה במהלך הניגון של הקובץ. בדוק את הלוגים בהגדרות.",
|
"an_error_occured_while_playing_the_video": "קרתה תקלה במהלך הניגון של הקובץ. בדוק את הלוגים בהגדרות.",
|
||||||
@@ -606,9 +699,37 @@
|
|||||||
"go_back": "חזור",
|
"go_back": "חזור",
|
||||||
"downloaded_file_title": "You have this file downloaded",
|
"downloaded_file_title": "You have this file downloaded",
|
||||||
"downloaded_file_message": "Do you want to play the downloaded file?",
|
"downloaded_file_message": "Do you want to play the downloaded file?",
|
||||||
"downloaded_file_yes": "Yes",
|
"downloaded_file_yes": "כן",
|
||||||
"downloaded_file_no": "No",
|
"downloaded_file_no": "לא",
|
||||||
"downloaded_file_cancel": "Cancel"
|
"downloaded_file_cancel": "Cancel",
|
||||||
|
"swipe_down_settings": "Swipe down for settings",
|
||||||
|
"ends_at": "Ends at {{time}}",
|
||||||
|
"search_subtitles": "Search Subtitles",
|
||||||
|
"subtitle_tracks": "Tracks",
|
||||||
|
"subtitle_search": "Search & Download",
|
||||||
|
"download": "Download",
|
||||||
|
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
|
||||||
|
"using_jellyfin_server": "Using Jellyfin Server",
|
||||||
|
"language": "Language",
|
||||||
|
"results": "Results",
|
||||||
|
"searching": "Searching...",
|
||||||
|
"search_failed": "Search failed",
|
||||||
|
"no_subtitle_provider": "No subtitle provider configured on server",
|
||||||
|
"no_subtitles_found": "No subtitles found",
|
||||||
|
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
|
||||||
|
"settings": "Settings",
|
||||||
|
"skip_intro": "Skip Intro",
|
||||||
|
"skip_credits": "Skip Credits",
|
||||||
|
"stopPlayback": "Stop Playback",
|
||||||
|
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
|
||||||
|
"stopPlayingConfirm": "Are you sure you want to stop playback?",
|
||||||
|
"downloaded": "Downloaded"
|
||||||
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "הבא בתור",
|
"next_up": "הבא בתור",
|
||||||
@@ -617,6 +738,11 @@
|
|||||||
"series": "סדרות",
|
"series": "סדרות",
|
||||||
"seasons": "עונות",
|
"seasons": "עונות",
|
||||||
"season": "עונה",
|
"season": "עונה",
|
||||||
|
"from_this_series": "From This Series",
|
||||||
|
"more_from_this_season": "More from this Season",
|
||||||
|
"view_series": "View Series",
|
||||||
|
"view_season": "View Season",
|
||||||
|
"select_season": "Select Season",
|
||||||
"no_episodes_for_this_season": "אין פרקים בעונה זו",
|
"no_episodes_for_this_season": "אין פרקים בעונה זו",
|
||||||
"overview": "סקירה",
|
"overview": "סקירה",
|
||||||
"more_with": "עוד עם {{name}}",
|
"more_with": "עוד עם {{name}}",
|
||||||
@@ -626,11 +752,22 @@
|
|||||||
"more_details": "פרטים נוספים",
|
"more_details": "פרטים נוספים",
|
||||||
"media_options": "Media Options",
|
"media_options": "Media Options",
|
||||||
"quality": "איכות",
|
"quality": "איכות",
|
||||||
"audio": "אודיו",
|
"audio": "שמע",
|
||||||
"subtitles": "כתובית",
|
"subtitles": {
|
||||||
|
"label": "Subtitle",
|
||||||
|
"none": "None",
|
||||||
|
"tracks": "Tracks"
|
||||||
|
},
|
||||||
"show_more": "הצג עוד",
|
"show_more": "הצג עוד",
|
||||||
"show_less": "הצג פחות",
|
"show_less": "הצג פחות",
|
||||||
|
"left": "left",
|
||||||
|
"more_info": "More Info",
|
||||||
|
"director": "Director",
|
||||||
|
"cast": "Cast",
|
||||||
|
"technical_details": "Technical Details",
|
||||||
"appeared_in": "הופיע ב-",
|
"appeared_in": "הופיע ב-",
|
||||||
|
"movies": "Movies",
|
||||||
|
"shows": "Shows",
|
||||||
"could_not_load_item": "נכשל בטעינת פריט",
|
"could_not_load_item": "נכשל בטעינת פריט",
|
||||||
"none": "ללא",
|
"none": "ללא",
|
||||||
"download": {
|
"download": {
|
||||||
@@ -641,7 +778,13 @@
|
|||||||
"download_x_item": "הורד {{item_count}} פריטים",
|
"download_x_item": "הורד {{item_count}} פריטים",
|
||||||
"download_unwatched_only": "רק שלא נצפו",
|
"download_unwatched_only": "רק שלא נצפו",
|
||||||
"download_button": "הורד"
|
"download_button": "הורד"
|
||||||
}
|
},
|
||||||
|
"mark_played": "Mark as Watched",
|
||||||
|
"mark_unplayed": "Mark as Unwatched",
|
||||||
|
"resume_playback": "Resume Playback",
|
||||||
|
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
|
||||||
|
"play_from_start": "Play from Start",
|
||||||
|
"continue_from": "Continue from {{time}}"
|
||||||
},
|
},
|
||||||
"live_tv": {
|
"live_tv": {
|
||||||
"next": "הבא",
|
"next": "הבא",
|
||||||
@@ -652,7 +795,18 @@
|
|||||||
"movies": "סרטים",
|
"movies": "סרטים",
|
||||||
"sports": "ספורט",
|
"sports": "ספורט",
|
||||||
"for_kids": "לילדים",
|
"for_kids": "לילדים",
|
||||||
"news": "חדשות"
|
"news": "חדשות",
|
||||||
|
"page_of": "Page {{current}} of {{total}}",
|
||||||
|
"no_programs": "No programs available",
|
||||||
|
"no_channels": "No channels available",
|
||||||
|
"tabs": {
|
||||||
|
"programs": "Programs",
|
||||||
|
"guide": "Guide",
|
||||||
|
"channels": "Channels",
|
||||||
|
"recordings": "Recordings",
|
||||||
|
"schedule": "Schedule",
|
||||||
|
"series": "Series"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"jellyseerr": {
|
"jellyseerr": {
|
||||||
"confirm": "אשר",
|
"confirm": "אשר",
|
||||||
@@ -697,6 +851,12 @@
|
|||||||
"decline": "Decline",
|
"decline": "Decline",
|
||||||
"requested_by": "Requested by {{user}}",
|
"requested_by": "Requested by {{user}}",
|
||||||
"unknown_user": "Unknown User",
|
"unknown_user": "Unknown User",
|
||||||
|
"select": "Select",
|
||||||
|
"request_all": "Request All",
|
||||||
|
"request_seasons": "Request Seasons",
|
||||||
|
"select_seasons": "Select Seasons",
|
||||||
|
"request_selected": "Request Selected",
|
||||||
|
"n_selected": "{{count}} selected",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"jellyseer_does_not_meet_requirements": "שרת ה-Seerr לא תואם את הגרסה המינימלית הנרדת! אנא עדכן לפחות לגרסה 2.0.0",
|
"jellyseer_does_not_meet_requirements": "שרת ה-Seerr לא תואם את הגרסה המינימלית הנרדת! אנא עדכן לפחות לגרסה 2.0.0",
|
||||||
"jellyseerr_test_failed": "בדיקת ה-Seerr נכשלה. אנא נסה שוב.",
|
"jellyseerr_test_failed": "בדיקת ה-Seerr נכשלה. אנא נסה שוב.",
|
||||||
@@ -716,13 +876,14 @@
|
|||||||
"search": "חיפוש",
|
"search": "חיפוש",
|
||||||
"library": "ספריה",
|
"library": "ספריה",
|
||||||
"custom_links": "קישורים מותאמים אישית",
|
"custom_links": "קישורים מותאמים אישית",
|
||||||
"favorites": "מועדפים"
|
"favorites": "מועדפים",
|
||||||
|
"settings": "Settings"
|
||||||
},
|
},
|
||||||
"music": {
|
"music": {
|
||||||
"title": "Music",
|
"title": "מוזיקה",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"suggestions": "Suggestions",
|
"suggestions": "Suggestions",
|
||||||
"albums": "Albums",
|
"albums": "אלבומים",
|
||||||
"artists": "Artists",
|
"artists": "Artists",
|
||||||
"playlists": "Playlists",
|
"playlists": "Playlists",
|
||||||
"tracks": "tracks"
|
"tracks": "tracks"
|
||||||
@@ -798,9 +959,9 @@
|
|||||||
"delete_button": "Delete",
|
"delete_button": "Delete",
|
||||||
"remove_button": "Remove",
|
"remove_button": "Remove",
|
||||||
"cancel_button": "Cancel",
|
"cancel_button": "Cancel",
|
||||||
"name_label": "Name",
|
"name_label": "שם",
|
||||||
"name_placeholder": "Enter watchlist name",
|
"name_placeholder": "Enter watchlist name",
|
||||||
"description_label": "Description",
|
"description_label": "תיאור",
|
||||||
"description_placeholder": "Enter description (optional)",
|
"description_placeholder": "Enter description (optional)",
|
||||||
"is_public_label": "Public Watchlist",
|
"is_public_label": "Public Watchlist",
|
||||||
"is_public_description": "Allow others to view this watchlist",
|
"is_public_description": "Allow others to view this watchlist",
|
||||||
@@ -817,10 +978,10 @@
|
|||||||
"remove_from_watchlist": "Remove from Watchlist",
|
"remove_from_watchlist": "Remove from Watchlist",
|
||||||
"select_watchlist": "Select Watchlist",
|
"select_watchlist": "Select Watchlist",
|
||||||
"create_new": "Create New Watchlist",
|
"create_new": "Create New Watchlist",
|
||||||
"item": "item",
|
"item": "פריט",
|
||||||
"items": "items",
|
"items": "פריטים",
|
||||||
"public": "Public",
|
"public": "ציבורי",
|
||||||
"private": "Private",
|
"private": "פרטי",
|
||||||
"you": "You",
|
"you": "You",
|
||||||
"by_owner": "By another user",
|
"by_owner": "By another user",
|
||||||
"not_found": "Watchlist not found",
|
"not_found": "Watchlist not found",
|
||||||
@@ -835,11 +996,42 @@
|
|||||||
"playback_speed": {
|
"playback_speed": {
|
||||||
"title": "Playback Speed",
|
"title": "Playback Speed",
|
||||||
"apply_to": "Apply To",
|
"apply_to": "Apply To",
|
||||||
"speed": "Speed",
|
"speed": "מהירות",
|
||||||
"scope": {
|
"scope": {
|
||||||
"media": "This media only",
|
"media": "This media only",
|
||||||
"show": "This show",
|
"show": "This show",
|
||||||
"all": "All media (default)"
|
"all": "All media (default)"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"companion_login": {
|
||||||
|
"title": "Pair with TV",
|
||||||
|
"align_qr": "Align the QR code within the frame",
|
||||||
|
"enter_code_manually": "Enter code manually",
|
||||||
|
"pairing_enter_credentials": "Enter credentials for TV",
|
||||||
|
"pairing_code_label": "Pairing code",
|
||||||
|
"server": "Server",
|
||||||
|
"authorize_button": "Authorize",
|
||||||
|
"authorizing": "Authorizing...",
|
||||||
|
"scan_again": "Scan Again",
|
||||||
|
"done": "Done",
|
||||||
|
"success_title": "Authorization Sent",
|
||||||
|
"pairing_tv_connecting": "The TV is connecting to your account",
|
||||||
|
"error_title": "Authorization Failed",
|
||||||
|
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
|
||||||
|
"error_generic": "Something went wrong. Please try again.",
|
||||||
|
"error_permission_denied": "Camera permission is required to scan QR codes.",
|
||||||
|
"login_as": "Log in as {{username}}?",
|
||||||
|
"on_server": "on {{server}}",
|
||||||
|
"use_different_user": "Use a different user",
|
||||||
|
"open_settings": "Open Settings"
|
||||||
|
},
|
||||||
|
"pairing": {
|
||||||
|
"pair_with_phone": "Pair with Phone",
|
||||||
|
"pair_with_phone_title": "Login TV",
|
||||||
|
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
|
||||||
|
"waiting_for_phone": "Waiting for phone...",
|
||||||
|
"scan_with_phone": "Scan with the Streamyfin app on your phone",
|
||||||
|
"logging_in": "Logging in...",
|
||||||
|
"logging_in_description": "Connecting to your server"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
"error_title": "Hiba",
|
"error_title": "Hiba",
|
||||||
"login_title": "Bejelentkezés",
|
"login_title": "Bejelentkezés",
|
||||||
"login_to_title": "Bejelentkezés ide",
|
"login_to_title": "Bejelentkezés ide",
|
||||||
|
"select_user": "Select a user to log in",
|
||||||
|
"add_user_to_login": "Add a user to log in",
|
||||||
|
"add_user": "Add User",
|
||||||
"username_placeholder": "Felhasználónév",
|
"username_placeholder": "Felhasználónév",
|
||||||
"password_placeholder": "Jelszó",
|
"password_placeholder": "Jelszó",
|
||||||
"login_button": "Bejelentkezés",
|
"login_button": "Bejelentkezés",
|
||||||
@@ -42,7 +45,13 @@
|
|||||||
"accounts_count": "{{count}} accounts",
|
"accounts_count": "{{count}} accounts",
|
||||||
"select_account": "Select Account",
|
"select_account": "Select Account",
|
||||||
"add_account": "Add Account",
|
"add_account": "Add Account",
|
||||||
"remove_account_description": "This will remove the saved credentials for {{username}}."
|
"remove_account_description": "This will remove the saved credentials for {{username}}.",
|
||||||
|
"remove_server": "Remove Server",
|
||||||
|
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
|
||||||
|
"select_your_server": "Select Your Server",
|
||||||
|
"add_server_to_get_started": "Add a server to get started",
|
||||||
|
"add_server": "Add Server",
|
||||||
|
"change_server": "Change Server"
|
||||||
},
|
},
|
||||||
"save_account": {
|
"save_account": {
|
||||||
"title": "Save Account",
|
"title": "Save Account",
|
||||||
@@ -86,6 +95,7 @@
|
|||||||
"oops": "Hoppá!",
|
"oops": "Hoppá!",
|
||||||
"error_message": "Valami nem stimmel.\nKérjük, jelentkezz ki, majd újra be.",
|
"error_message": "Valami nem stimmel.\nKérjük, jelentkezz ki, majd újra be.",
|
||||||
"continue_watching": "Nézd Tovább",
|
"continue_watching": "Nézd Tovább",
|
||||||
|
"continue": "Continue",
|
||||||
"next_up": "Következő",
|
"next_up": "Következő",
|
||||||
"continue_and_next_up": "Continue & Next Up",
|
"continue_and_next_up": "Continue & Next Up",
|
||||||
"recently_added_in": "Új a(z) {{libraryName}} könyvtárban",
|
"recently_added_in": "Új a(z) {{libraryName}} könyvtárban",
|
||||||
@@ -109,6 +119,12 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"settings_title": "Beállítások",
|
"settings_title": "Beállítások",
|
||||||
"log_out_button": "Kijelentkezés",
|
"log_out_button": "Kijelentkezés",
|
||||||
|
"switch_user": {
|
||||||
|
"title": "Switch User",
|
||||||
|
"account": "Account",
|
||||||
|
"switch_user": "Switch User on This Server",
|
||||||
|
"current": "current"
|
||||||
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "Categories"
|
"title": "Categories"
|
||||||
},
|
},
|
||||||
@@ -121,7 +137,16 @@
|
|||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "Appearance",
|
"title": "Appearance",
|
||||||
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
||||||
"hide_remote_session_button": "Hide Remote Session Button"
|
"hide_remote_session_button": "Hide Remote Session Button",
|
||||||
|
"show_home_backdrop": "Dynamic Home Backdrop",
|
||||||
|
"show_hero_carousel": "Hero Carousel",
|
||||||
|
"show_series_poster_on_episode": "Show Series Poster on Episodes",
|
||||||
|
"theme_music": "Theme Music",
|
||||||
|
"display_size": "Display Size",
|
||||||
|
"display_size_small": "Small",
|
||||||
|
"display_size_default": "Default",
|
||||||
|
"display_size_large": "Large",
|
||||||
|
"display_size_extra_large": "Extra Large"
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"title": "Network",
|
"title": "Network",
|
||||||
@@ -174,6 +199,22 @@
|
|||||||
"rewind_length": "Visszatekerés Hossza",
|
"rewind_length": "Visszatekerés Hossza",
|
||||||
"seconds_unit": "mp"
|
"seconds_unit": "mp"
|
||||||
},
|
},
|
||||||
|
"buffer": {
|
||||||
|
"title": "Buffer Settings",
|
||||||
|
"cache_mode": "Cache Mode",
|
||||||
|
"cache_auto": "Auto",
|
||||||
|
"cache_yes": "Enabled",
|
||||||
|
"cache_no": "Disabled",
|
||||||
|
"buffer_duration": "Buffer Duration",
|
||||||
|
"max_cache_size": "Max Cache Size",
|
||||||
|
"max_backward_cache": "Max Backward Cache"
|
||||||
|
},
|
||||||
|
"vo_driver": {
|
||||||
|
"title": "Video Output",
|
||||||
|
"vo_mode": "VO Driver",
|
||||||
|
"gpu_next": "gpu-next (Recommended)",
|
||||||
|
"gpu": "gpu"
|
||||||
|
},
|
||||||
"gesture_controls": {
|
"gesture_controls": {
|
||||||
"gesture_controls_title": "Gesztusvezérlés",
|
"gesture_controls_title": "Gesztusvezérlés",
|
||||||
"horizontal_swipe_skip": "Vízszintes Húzás Ugráshoz",
|
"horizontal_swipe_skip": "Vízszintes Húzás Ugráshoz",
|
||||||
@@ -256,7 +297,23 @@
|
|||||||
"subtitle_font": "Subtitle Font",
|
"subtitle_font": "Subtitle Font",
|
||||||
"ksplayer_title": "KSPlayer Settings",
|
"ksplayer_title": "KSPlayer Settings",
|
||||||
"hardware_decode": "Hardware Decoding",
|
"hardware_decode": "Hardware Decoding",
|
||||||
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
|
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.",
|
||||||
|
"opensubtitles_title": "OpenSubtitles",
|
||||||
|
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
|
||||||
|
"opensubtitles_api_key": "API Key",
|
||||||
|
"opensubtitles_api_key_placeholder": "Enter API key...",
|
||||||
|
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
|
||||||
|
"mpv_subtitle_scale": "Subtitle Scale",
|
||||||
|
"mpv_subtitle_margin_y": "Vertical Margin",
|
||||||
|
"mpv_subtitle_align_x": "Horizontal Align",
|
||||||
|
"mpv_subtitle_align_y": "Vertical Align",
|
||||||
|
"align": {
|
||||||
|
"left": "Left",
|
||||||
|
"center": "Center",
|
||||||
|
"right": "Right",
|
||||||
|
"top": "Top",
|
||||||
|
"bottom": "Bottom"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"vlc_subtitles": {
|
"vlc_subtitles": {
|
||||||
"title": "VLC Subtitle Settings",
|
"title": "VLC Subtitle Settings",
|
||||||
@@ -406,7 +463,13 @@
|
|||||||
"music_cache_cleared": "Music cache cleared",
|
"music_cache_cleared": "Music cache cleared",
|
||||||
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
||||||
"downloaded_songs_size": "{{size}} downloaded",
|
"downloaded_songs_size": "{{size}} downloaded",
|
||||||
"downloaded_songs_deleted": "Downloaded songs deleted"
|
"downloaded_songs_deleted": "Downloaded songs deleted",
|
||||||
|
"clear_all_cache": "Clear All Cache",
|
||||||
|
"clear_all_cache_confirm": "Clear All Cache?",
|
||||||
|
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
|
||||||
|
"clear_all_cache_success": "Cache Cleared",
|
||||||
|
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
|
||||||
|
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "Intro",
|
"title": "Intro",
|
||||||
@@ -430,6 +493,21 @@
|
|||||||
"error_deleting_files": "Hiba a Fájlok Törlésekor",
|
"error_deleting_files": "Hiba a Fájlok Törlésekor",
|
||||||
"background_downloads_enabled": "Background downloads enabled",
|
"background_downloads_enabled": "Background downloads enabled",
|
||||||
"background_downloads_disabled": "Background downloads disabled"
|
"background_downloads_disabled": "Background downloads disabled"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"title": "Security",
|
||||||
|
"inactivity_timeout": {
|
||||||
|
"title": "Inactivity Timeout",
|
||||||
|
"description": "Auto logout after inactivity",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"1_minute": "1 minute",
|
||||||
|
"5_minutes": "5 minutes",
|
||||||
|
"15_minutes": "15 minutes",
|
||||||
|
"30_minutes": "30 minutes",
|
||||||
|
"1_hour": "1 hour",
|
||||||
|
"4_hours": "4 hours",
|
||||||
|
"24_hours": "24 hours"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -456,6 +534,7 @@
|
|||||||
"new_app_version_requires_re_download_description": "Az új frissítéshez az összes tartalmat újra le kell tölteni. Kérjük, töröld az összes letöltött tartalmat, majd próbáld újra.",
|
"new_app_version_requires_re_download_description": "Az új frissítéshez az összes tartalmat újra le kell tölteni. Kérjük, töröld az összes letöltött tartalmat, majd próbáld újra.",
|
||||||
"back": "Vissza",
|
"back": "Vissza",
|
||||||
"delete": "Törlés",
|
"delete": "Törlés",
|
||||||
|
"delete_download": "Delete Download",
|
||||||
"something_went_wrong": "Hiba Történt",
|
"something_went_wrong": "Hiba Történt",
|
||||||
"could_not_get_stream_url_from_jellyfin": "Nem sikerült lekérni a stream URL-t a Jellyfinből",
|
"could_not_get_stream_url_from_jellyfin": "Nem sikerült lekérni a stream URL-t a Jellyfinből",
|
||||||
"eta": "Várható Idő: {{eta}}",
|
"eta": "Várható Idő: {{eta}}",
|
||||||
@@ -492,22 +571,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"no_results": "No Results",
|
||||||
"select": "Select",
|
"select": "Select",
|
||||||
"no_trailer_available": "No trailer available",
|
"no_trailer_available": "No trailer available",
|
||||||
"video": "Videó",
|
"video": "Videó",
|
||||||
"audio": "Hang",
|
"audio": "Hang",
|
||||||
"subtitle": "Felirat",
|
"subtitle": "Felirat",
|
||||||
"play": "Play",
|
"play": "Play",
|
||||||
|
"mark_as_played": "Mark as Played",
|
||||||
|
"mark_as_not_played": "Mark as not Played",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
"track": "Track",
|
"track": "Track",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
|
"stop": "Stop",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"verifying": "Verifying..."
|
"verifying": "Verifying...",
|
||||||
|
"login": "Login",
|
||||||
|
"refresh": "Refresh"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"search": "Keresés...",
|
"search": "Keresés...",
|
||||||
@@ -556,6 +641,7 @@
|
|||||||
"movies": "Filmek",
|
"movies": "Filmek",
|
||||||
"series": "Sorozatok",
|
"series": "Sorozatok",
|
||||||
"boxsets": "Gyűjtemények",
|
"boxsets": "Gyűjtemények",
|
||||||
|
"playlists": "Playlists",
|
||||||
"items": "Elemek"
|
"items": "Elemek"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
@@ -566,7 +652,8 @@
|
|||||||
"poster": "Poszter",
|
"poster": "Poszter",
|
||||||
"cover": "Borító",
|
"cover": "Borító",
|
||||||
"show_titles": "Címek Megjelenítése",
|
"show_titles": "Címek Megjelenítése",
|
||||||
"show_stats": "Statisztikák Megjelenítése"
|
"show_stats": "Statisztikák Megjelenítése",
|
||||||
|
"options_title": "Options"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"genres": "Műfajok",
|
"genres": "Műfajok",
|
||||||
@@ -574,7 +661,11 @@
|
|||||||
"sort_by": "Rendezés",
|
"sort_by": "Rendezés",
|
||||||
"filter_by": "Filter By",
|
"filter_by": "Filter By",
|
||||||
"sort_order": "Rendezés Iránya",
|
"sort_order": "Rendezés Iránya",
|
||||||
"tags": "Címkék"
|
"tags": "Címkék",
|
||||||
|
"all": "All",
|
||||||
|
"reset": "Reset",
|
||||||
|
"asc": "Ascending",
|
||||||
|
"desc": "Descending"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"favorites": {
|
"favorites": {
|
||||||
@@ -591,6 +682,8 @@
|
|||||||
"no_links": "Nincsenek Linkek"
|
"no_links": "Nincsenek Linkek"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
|
"live": "LIVE",
|
||||||
|
"mpv_player_title": "MPV Player",
|
||||||
"error": "Hiba",
|
"error": "Hiba",
|
||||||
"failed_to_get_stream_url": "Nem sikerült lekérni a stream URL-t",
|
"failed_to_get_stream_url": "Nem sikerült lekérni a stream URL-t",
|
||||||
"an_error_occured_while_playing_the_video": "Hiba történt a videó lejátszása közben. Ellenőrizd a naplókat a beállításokban.",
|
"an_error_occured_while_playing_the_video": "Hiba történt a videó lejátszása közben. Ellenőrizd a naplókat a beállításokban.",
|
||||||
@@ -608,7 +701,35 @@
|
|||||||
"downloaded_file_message": "Do you want to play the downloaded file?",
|
"downloaded_file_message": "Do you want to play the downloaded file?",
|
||||||
"downloaded_file_yes": "Yes",
|
"downloaded_file_yes": "Yes",
|
||||||
"downloaded_file_no": "No",
|
"downloaded_file_no": "No",
|
||||||
"downloaded_file_cancel": "Cancel"
|
"downloaded_file_cancel": "Cancel",
|
||||||
|
"swipe_down_settings": "Swipe down for settings",
|
||||||
|
"ends_at": "Ends at {{time}}",
|
||||||
|
"search_subtitles": "Search Subtitles",
|
||||||
|
"subtitle_tracks": "Tracks",
|
||||||
|
"subtitle_search": "Search & Download",
|
||||||
|
"download": "Download",
|
||||||
|
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
|
||||||
|
"using_jellyfin_server": "Using Jellyfin Server",
|
||||||
|
"language": "Language",
|
||||||
|
"results": "Results",
|
||||||
|
"searching": "Searching...",
|
||||||
|
"search_failed": "Search failed",
|
||||||
|
"no_subtitle_provider": "No subtitle provider configured on server",
|
||||||
|
"no_subtitles_found": "No subtitles found",
|
||||||
|
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
|
||||||
|
"settings": "Settings",
|
||||||
|
"skip_intro": "Skip Intro",
|
||||||
|
"skip_credits": "Skip Credits",
|
||||||
|
"stopPlayback": "Stop Playback",
|
||||||
|
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
|
||||||
|
"stopPlayingConfirm": "Are you sure you want to stop playback?",
|
||||||
|
"downloaded": "Downloaded"
|
||||||
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Következő",
|
"next_up": "Következő",
|
||||||
@@ -617,6 +738,11 @@
|
|||||||
"series": "Sorozat",
|
"series": "Sorozat",
|
||||||
"seasons": "Évadok",
|
"seasons": "Évadok",
|
||||||
"season": "Évad",
|
"season": "Évad",
|
||||||
|
"from_this_series": "From This Series",
|
||||||
|
"more_from_this_season": "More from this Season",
|
||||||
|
"view_series": "View Series",
|
||||||
|
"view_season": "View Season",
|
||||||
|
"select_season": "Select Season",
|
||||||
"no_episodes_for_this_season": "Ehhez az évadhoz nincs epizód",
|
"no_episodes_for_this_season": "Ehhez az évadhoz nincs epizód",
|
||||||
"overview": "Áttekintés",
|
"overview": "Áttekintés",
|
||||||
"more_with": "További {{name}} Alkotások",
|
"more_with": "További {{name}} Alkotások",
|
||||||
@@ -627,10 +753,21 @@
|
|||||||
"media_options": "Media Options",
|
"media_options": "Media Options",
|
||||||
"quality": "Minőség",
|
"quality": "Minőség",
|
||||||
"audio": "Hang",
|
"audio": "Hang",
|
||||||
"subtitles": "Felirat",
|
"subtitles": {
|
||||||
|
"label": "Subtitle",
|
||||||
|
"none": "None",
|
||||||
|
"tracks": "Tracks"
|
||||||
|
},
|
||||||
"show_more": "Több Megjelenítése",
|
"show_more": "Több Megjelenítése",
|
||||||
"show_less": "Kevesebb Megjelenítése",
|
"show_less": "Kevesebb Megjelenítése",
|
||||||
|
"left": "left",
|
||||||
|
"more_info": "More Info",
|
||||||
|
"director": "Director",
|
||||||
|
"cast": "Cast",
|
||||||
|
"technical_details": "Technical Details",
|
||||||
"appeared_in": "Megjelent:",
|
"appeared_in": "Megjelent:",
|
||||||
|
"movies": "Movies",
|
||||||
|
"shows": "Shows",
|
||||||
"could_not_load_item": "Nem Sikerült Betölteni az Elemet",
|
"could_not_load_item": "Nem Sikerült Betölteni az Elemet",
|
||||||
"none": "Nincs",
|
"none": "Nincs",
|
||||||
"download": {
|
"download": {
|
||||||
@@ -641,7 +778,13 @@
|
|||||||
"download_x_item": "{{item_count}} Elem Letöltése",
|
"download_x_item": "{{item_count}} Elem Letöltése",
|
||||||
"download_unwatched_only": "Csak Nem Megtekintett",
|
"download_unwatched_only": "Csak Nem Megtekintett",
|
||||||
"download_button": "Letöltés"
|
"download_button": "Letöltés"
|
||||||
}
|
},
|
||||||
|
"mark_played": "Mark as Watched",
|
||||||
|
"mark_unplayed": "Mark as Unwatched",
|
||||||
|
"resume_playback": "Resume Playback",
|
||||||
|
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
|
||||||
|
"play_from_start": "Play from Start",
|
||||||
|
"continue_from": "Continue from {{time}}"
|
||||||
},
|
},
|
||||||
"live_tv": {
|
"live_tv": {
|
||||||
"next": "Következő",
|
"next": "Következő",
|
||||||
@@ -652,7 +795,18 @@
|
|||||||
"movies": "Filmek",
|
"movies": "Filmek",
|
||||||
"sports": "Sport",
|
"sports": "Sport",
|
||||||
"for_kids": "Gyerekeknek",
|
"for_kids": "Gyerekeknek",
|
||||||
"news": "Hírek"
|
"news": "Hírek",
|
||||||
|
"page_of": "Page {{current}} of {{total}}",
|
||||||
|
"no_programs": "No programs available",
|
||||||
|
"no_channels": "No channels available",
|
||||||
|
"tabs": {
|
||||||
|
"programs": "Programs",
|
||||||
|
"guide": "Guide",
|
||||||
|
"channels": "Channels",
|
||||||
|
"recordings": "Recordings",
|
||||||
|
"schedule": "Schedule",
|
||||||
|
"series": "Series"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"jellyseerr": {
|
"jellyseerr": {
|
||||||
"confirm": "Megerősítés",
|
"confirm": "Megerősítés",
|
||||||
@@ -697,6 +851,12 @@
|
|||||||
"decline": "Decline",
|
"decline": "Decline",
|
||||||
"requested_by": "Requested by {{user}}",
|
"requested_by": "Requested by {{user}}",
|
||||||
"unknown_user": "Unknown User",
|
"unknown_user": "Unknown User",
|
||||||
|
"select": "Select",
|
||||||
|
"request_all": "Request All",
|
||||||
|
"request_seasons": "Request Seasons",
|
||||||
|
"select_seasons": "Select Seasons",
|
||||||
|
"request_selected": "Request Selected",
|
||||||
|
"n_selected": "{{count}} selected",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"jellyseer_does_not_meet_requirements": "A Jellyseerr szerver nem felel meg a minimum verziókövetelményeknek! Kérlek frissítsd legalább 2.0.0-ra.",
|
"jellyseer_does_not_meet_requirements": "A Jellyseerr szerver nem felel meg a minimum verziókövetelményeknek! Kérlek frissítsd legalább 2.0.0-ra.",
|
||||||
"jellyseerr_test_failed": "A Jellyseerr teszt sikertelen. Próbáld újra.",
|
"jellyseerr_test_failed": "A Jellyseerr teszt sikertelen. Próbáld újra.",
|
||||||
@@ -716,7 +876,8 @@
|
|||||||
"search": "Keresés",
|
"search": "Keresés",
|
||||||
"library": "Könyvtár",
|
"library": "Könyvtár",
|
||||||
"custom_links": "Egyéni Linkek",
|
"custom_links": "Egyéni Linkek",
|
||||||
"favorites": "Kedvencek"
|
"favorites": "Kedvencek",
|
||||||
|
"settings": "Settings"
|
||||||
},
|
},
|
||||||
"music": {
|
"music": {
|
||||||
"title": "Music",
|
"title": "Music",
|
||||||
@@ -841,5 +1002,36 @@
|
|||||||
"show": "This show",
|
"show": "This show",
|
||||||
"all": "All media (default)"
|
"all": "All media (default)"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"companion_login": {
|
||||||
|
"title": "Pair with TV",
|
||||||
|
"align_qr": "Align the QR code within the frame",
|
||||||
|
"enter_code_manually": "Enter code manually",
|
||||||
|
"pairing_enter_credentials": "Enter credentials for TV",
|
||||||
|
"pairing_code_label": "Pairing code",
|
||||||
|
"server": "Server",
|
||||||
|
"authorize_button": "Authorize",
|
||||||
|
"authorizing": "Authorizing...",
|
||||||
|
"scan_again": "Scan Again",
|
||||||
|
"done": "Done",
|
||||||
|
"success_title": "Authorization Sent",
|
||||||
|
"pairing_tv_connecting": "The TV is connecting to your account",
|
||||||
|
"error_title": "Authorization Failed",
|
||||||
|
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
|
||||||
|
"error_generic": "Something went wrong. Please try again.",
|
||||||
|
"error_permission_denied": "Camera permission is required to scan QR codes.",
|
||||||
|
"login_as": "Log in as {{username}}?",
|
||||||
|
"on_server": "on {{server}}",
|
||||||
|
"use_different_user": "Use a different user",
|
||||||
|
"open_settings": "Open Settings"
|
||||||
|
},
|
||||||
|
"pairing": {
|
||||||
|
"pair_with_phone": "Pair with Phone",
|
||||||
|
"pair_with_phone_title": "Login TV",
|
||||||
|
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
|
||||||
|
"waiting_for_phone": "Waiting for phone...",
|
||||||
|
"scan_with_phone": "Scan with the Streamyfin app on your phone",
|
||||||
|
"logging_in": "Logging in...",
|
||||||
|
"logging_in_description": "Connecting to your server"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
"error_title": "Errore",
|
"error_title": "Errore",
|
||||||
"login_title": "Accesso",
|
"login_title": "Accesso",
|
||||||
"login_to_title": "Accedi a",
|
"login_to_title": "Accedi a",
|
||||||
|
"select_user": "Select a user to log in",
|
||||||
|
"add_user_to_login": "Add a user to log in",
|
||||||
|
"add_user": "Add User",
|
||||||
"username_placeholder": "Nome utente",
|
"username_placeholder": "Nome utente",
|
||||||
"password_placeholder": "Password",
|
"password_placeholder": "Password",
|
||||||
"login_button": "Accedi",
|
"login_button": "Accedi",
|
||||||
@@ -42,7 +45,13 @@
|
|||||||
"accounts_count": "{{count}} accounts",
|
"accounts_count": "{{count}} accounts",
|
||||||
"select_account": "Select Account",
|
"select_account": "Select Account",
|
||||||
"add_account": "Add Account",
|
"add_account": "Add Account",
|
||||||
"remove_account_description": "This will remove the saved credentials for {{username}}."
|
"remove_account_description": "This will remove the saved credentials for {{username}}.",
|
||||||
|
"remove_server": "Remove Server",
|
||||||
|
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
|
||||||
|
"select_your_server": "Select Your Server",
|
||||||
|
"add_server_to_get_started": "Add a server to get started",
|
||||||
|
"add_server": "Add Server",
|
||||||
|
"change_server": "Change Server"
|
||||||
},
|
},
|
||||||
"save_account": {
|
"save_account": {
|
||||||
"title": "Save Account",
|
"title": "Save Account",
|
||||||
@@ -86,6 +95,7 @@
|
|||||||
"oops": "Ops!",
|
"oops": "Ops!",
|
||||||
"error_message": "Qualcosa è andato storto. \nEffetturare il logout e riaccedere.",
|
"error_message": "Qualcosa è andato storto. \nEffetturare il logout e riaccedere.",
|
||||||
"continue_watching": "Continua a guardare",
|
"continue_watching": "Continua a guardare",
|
||||||
|
"continue": "Continue",
|
||||||
"next_up": "Prossimo",
|
"next_up": "Prossimo",
|
||||||
"continue_and_next_up": "Continue & Next Up",
|
"continue_and_next_up": "Continue & Next Up",
|
||||||
"recently_added_in": "Aggiunti di recente a {{libraryName}}",
|
"recently_added_in": "Aggiunti di recente a {{libraryName}}",
|
||||||
@@ -109,6 +119,12 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"settings_title": "Impostazioni",
|
"settings_title": "Impostazioni",
|
||||||
"log_out_button": "Esci",
|
"log_out_button": "Esci",
|
||||||
|
"switch_user": {
|
||||||
|
"title": "Switch User",
|
||||||
|
"account": "Account",
|
||||||
|
"switch_user": "Switch User on This Server",
|
||||||
|
"current": "current"
|
||||||
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "Categorie"
|
"title": "Categorie"
|
||||||
},
|
},
|
||||||
@@ -121,7 +137,16 @@
|
|||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "Aspetto",
|
"title": "Aspetto",
|
||||||
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
||||||
"hide_remote_session_button": "Hide Remote Session Button"
|
"hide_remote_session_button": "Hide Remote Session Button",
|
||||||
|
"show_home_backdrop": "Dynamic Home Backdrop",
|
||||||
|
"show_hero_carousel": "Hero Carousel",
|
||||||
|
"show_series_poster_on_episode": "Show Series Poster on Episodes",
|
||||||
|
"theme_music": "Theme Music",
|
||||||
|
"display_size": "Display Size",
|
||||||
|
"display_size_small": "Small",
|
||||||
|
"display_size_default": "Default",
|
||||||
|
"display_size_large": "Large",
|
||||||
|
"display_size_extra_large": "Extra Large"
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"title": "Network",
|
"title": "Network",
|
||||||
@@ -136,7 +161,7 @@
|
|||||||
"not_connected_to_wifi": "Not connected to WiFi",
|
"not_connected_to_wifi": "Not connected to WiFi",
|
||||||
"no_networks_configured": "No networks configured",
|
"no_networks_configured": "No networks configured",
|
||||||
"add_network_hint": "Add your home WiFi network to enable auto-switching",
|
"add_network_hint": "Add your home WiFi network to enable auto-switching",
|
||||||
"current_wifi": "Current WiFi",
|
"current_wifi": "WiFi Attuale",
|
||||||
"using_url": "Sta utilizzando",
|
"using_url": "Sta utilizzando",
|
||||||
"local": "Local URL",
|
"local": "Local URL",
|
||||||
"remote": "Remote URL",
|
"remote": "Remote URL",
|
||||||
@@ -174,6 +199,22 @@
|
|||||||
"rewind_length": "Lunghezza del riavvolgimento",
|
"rewind_length": "Lunghezza del riavvolgimento",
|
||||||
"seconds_unit": "secondi"
|
"seconds_unit": "secondi"
|
||||||
},
|
},
|
||||||
|
"buffer": {
|
||||||
|
"title": "Buffer Settings",
|
||||||
|
"cache_mode": "Cache Mode",
|
||||||
|
"cache_auto": "Auto",
|
||||||
|
"cache_yes": "Enabled",
|
||||||
|
"cache_no": "Disabled",
|
||||||
|
"buffer_duration": "Buffer Duration",
|
||||||
|
"max_cache_size": "Max Cache Size",
|
||||||
|
"max_backward_cache": "Max Backward Cache"
|
||||||
|
},
|
||||||
|
"vo_driver": {
|
||||||
|
"title": "Video Output",
|
||||||
|
"vo_mode": "VO Driver",
|
||||||
|
"gpu_next": "gpu-next (Recommended)",
|
||||||
|
"gpu": "gpu"
|
||||||
|
},
|
||||||
"gesture_controls": {
|
"gesture_controls": {
|
||||||
"gesture_controls_title": "Controlli Gesture",
|
"gesture_controls_title": "Controlli Gesture",
|
||||||
"horizontal_swipe_skip": "Scorrimento orizzontale per saltare",
|
"horizontal_swipe_skip": "Scorrimento orizzontale per saltare",
|
||||||
@@ -256,7 +297,23 @@
|
|||||||
"subtitle_font": "Subtitle Font",
|
"subtitle_font": "Subtitle Font",
|
||||||
"ksplayer_title": "KSPlayer Settings",
|
"ksplayer_title": "KSPlayer Settings",
|
||||||
"hardware_decode": "Hardware Decoding",
|
"hardware_decode": "Hardware Decoding",
|
||||||
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
|
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.",
|
||||||
|
"opensubtitles_title": "OpenSubtitles",
|
||||||
|
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
|
||||||
|
"opensubtitles_api_key": "API Key",
|
||||||
|
"opensubtitles_api_key_placeholder": "Enter API key...",
|
||||||
|
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
|
||||||
|
"mpv_subtitle_scale": "Subtitle Scale",
|
||||||
|
"mpv_subtitle_margin_y": "Vertical Margin",
|
||||||
|
"mpv_subtitle_align_x": "Horizontal Align",
|
||||||
|
"mpv_subtitle_align_y": "Vertical Align",
|
||||||
|
"align": {
|
||||||
|
"left": "Left",
|
||||||
|
"center": "Center",
|
||||||
|
"right": "Right",
|
||||||
|
"top": "Top",
|
||||||
|
"bottom": "Bottom"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"vlc_subtitles": {
|
"vlc_subtitles": {
|
||||||
"title": "VLC Subtitle Settings",
|
"title": "VLC Subtitle Settings",
|
||||||
@@ -406,7 +463,13 @@
|
|||||||
"music_cache_cleared": "Music cache cleared",
|
"music_cache_cleared": "Music cache cleared",
|
||||||
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
||||||
"downloaded_songs_size": "{{size}} downloaded",
|
"downloaded_songs_size": "{{size}} downloaded",
|
||||||
"downloaded_songs_deleted": "Downloaded songs deleted"
|
"downloaded_songs_deleted": "Downloaded songs deleted",
|
||||||
|
"clear_all_cache": "Clear All Cache",
|
||||||
|
"clear_all_cache_confirm": "Clear All Cache?",
|
||||||
|
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
|
||||||
|
"clear_all_cache_success": "Cache Cleared",
|
||||||
|
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
|
||||||
|
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "Intro",
|
"title": "Intro",
|
||||||
@@ -430,6 +493,21 @@
|
|||||||
"error_deleting_files": "Errore nella cancellazione dei file",
|
"error_deleting_files": "Errore nella cancellazione dei file",
|
||||||
"background_downloads_enabled": "Scaricamento in background abilitato",
|
"background_downloads_enabled": "Scaricamento in background abilitato",
|
||||||
"background_downloads_disabled": "Scaricamento in background disabilitato"
|
"background_downloads_disabled": "Scaricamento in background disabilitato"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"title": "Security",
|
||||||
|
"inactivity_timeout": {
|
||||||
|
"title": "Inactivity Timeout",
|
||||||
|
"description": "Auto logout after inactivity",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"1_minute": "1 minute",
|
||||||
|
"5_minutes": "5 minutes",
|
||||||
|
"15_minutes": "15 minutes",
|
||||||
|
"30_minutes": "30 minutes",
|
||||||
|
"1_hour": "1 hour",
|
||||||
|
"4_hours": "4 hours",
|
||||||
|
"24_hours": "24 hours"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -456,6 +534,7 @@
|
|||||||
"new_app_version_requires_re_download_description": "Il nuovo aggiornamento richiede di scaricare nuovamente i contenuti. Rimuovere tutti i contenuti scaricati e riprovare.",
|
"new_app_version_requires_re_download_description": "Il nuovo aggiornamento richiede di scaricare nuovamente i contenuti. Rimuovere tutti i contenuti scaricati e riprovare.",
|
||||||
"back": "Indietro",
|
"back": "Indietro",
|
||||||
"delete": "Cancella",
|
"delete": "Cancella",
|
||||||
|
"delete_download": "Delete Download",
|
||||||
"something_went_wrong": "Qualcosa è andato storto",
|
"something_went_wrong": "Qualcosa è andato storto",
|
||||||
"could_not_get_stream_url_from_jellyfin": "Impossibile ottenere l'URL del flusso da Jellyfin",
|
"could_not_get_stream_url_from_jellyfin": "Impossibile ottenere l'URL del flusso da Jellyfin",
|
||||||
"eta": "Tempo stimato di completamento {{eta}}",
|
"eta": "Tempo stimato di completamento {{eta}}",
|
||||||
@@ -492,22 +571,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"no_results": "No Results",
|
||||||
"select": "Seleziona",
|
"select": "Seleziona",
|
||||||
"no_trailer_available": "Nessun trailer disponibile",
|
"no_trailer_available": "Nessun trailer disponibile",
|
||||||
"video": "Video",
|
"video": "Video",
|
||||||
"audio": "Audio",
|
"audio": "Audio",
|
||||||
"subtitle": "Sottotitoli",
|
"subtitle": "Sottotitoli",
|
||||||
"play": "Gioca",
|
"play": "Gioca",
|
||||||
|
"mark_as_played": "Mark as Played",
|
||||||
|
"mark_as_not_played": "Mark as not Played",
|
||||||
"none": "Nulla",
|
"none": "Nulla",
|
||||||
"track": "Traccia",
|
"track": "Traccia",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
|
"stop": "Stop",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"verifying": "Verifying..."
|
"verifying": "Verifying...",
|
||||||
|
"login": "Login",
|
||||||
|
"refresh": "Refresh"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"search": "Cerca...",
|
"search": "Cerca...",
|
||||||
@@ -556,6 +641,7 @@
|
|||||||
"movies": "film",
|
"movies": "film",
|
||||||
"series": "serie TV",
|
"series": "serie TV",
|
||||||
"boxsets": "cofanetti",
|
"boxsets": "cofanetti",
|
||||||
|
"playlists": "Playlists",
|
||||||
"items": "elementi"
|
"items": "elementi"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
@@ -566,7 +652,8 @@
|
|||||||
"poster": "Poster",
|
"poster": "Poster",
|
||||||
"cover": "Copertina",
|
"cover": "Copertina",
|
||||||
"show_titles": "Mostra titoli",
|
"show_titles": "Mostra titoli",
|
||||||
"show_stats": "Mostra statistiche"
|
"show_stats": "Mostra statistiche",
|
||||||
|
"options_title": "Options"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"genres": "Generi",
|
"genres": "Generi",
|
||||||
@@ -574,7 +661,11 @@
|
|||||||
"sort_by": "Ordina per",
|
"sort_by": "Ordina per",
|
||||||
"filter_by": "Filter By",
|
"filter_by": "Filter By",
|
||||||
"sort_order": "Criterio di ordinamento",
|
"sort_order": "Criterio di ordinamento",
|
||||||
"tags": "Tag"
|
"tags": "Tag",
|
||||||
|
"all": "All",
|
||||||
|
"reset": "Reset",
|
||||||
|
"asc": "Ascending",
|
||||||
|
"desc": "Descending"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"favorites": {
|
"favorites": {
|
||||||
@@ -591,6 +682,8 @@
|
|||||||
"no_links": "Nessun link"
|
"no_links": "Nessun link"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
|
"live": "LIVE",
|
||||||
|
"mpv_player_title": "MPV Player",
|
||||||
"error": "Errore",
|
"error": "Errore",
|
||||||
"failed_to_get_stream_url": "Impossibile ottenere l'URL dello stream",
|
"failed_to_get_stream_url": "Impossibile ottenere l'URL dello stream",
|
||||||
"an_error_occured_while_playing_the_video": "Si è verificato un errore durante la riproduzione del video. Controllare i log nelle impostazioni.",
|
"an_error_occured_while_playing_the_video": "Si è verificato un errore durante la riproduzione del video. Controllare i log nelle impostazioni.",
|
||||||
@@ -608,7 +701,35 @@
|
|||||||
"downloaded_file_message": "Do you want to play the downloaded file?",
|
"downloaded_file_message": "Do you want to play the downloaded file?",
|
||||||
"downloaded_file_yes": "Yes",
|
"downloaded_file_yes": "Yes",
|
||||||
"downloaded_file_no": "No",
|
"downloaded_file_no": "No",
|
||||||
"downloaded_file_cancel": "Cancel"
|
"downloaded_file_cancel": "Cancel",
|
||||||
|
"swipe_down_settings": "Swipe down for settings",
|
||||||
|
"ends_at": "Ends at {{time}}",
|
||||||
|
"search_subtitles": "Search Subtitles",
|
||||||
|
"subtitle_tracks": "Tracks",
|
||||||
|
"subtitle_search": "Search & Download",
|
||||||
|
"download": "Download",
|
||||||
|
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
|
||||||
|
"using_jellyfin_server": "Using Jellyfin Server",
|
||||||
|
"language": "Language",
|
||||||
|
"results": "Results",
|
||||||
|
"searching": "Searching...",
|
||||||
|
"search_failed": "Search failed",
|
||||||
|
"no_subtitle_provider": "No subtitle provider configured on server",
|
||||||
|
"no_subtitles_found": "No subtitles found",
|
||||||
|
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
|
||||||
|
"settings": "Settings",
|
||||||
|
"skip_intro": "Skip Intro",
|
||||||
|
"skip_credits": "Skip Credits",
|
||||||
|
"stopPlayback": "Stop Playback",
|
||||||
|
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
|
||||||
|
"stopPlayingConfirm": "Are you sure you want to stop playback?",
|
||||||
|
"downloaded": "Downloaded"
|
||||||
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Il prossimo",
|
"next_up": "Il prossimo",
|
||||||
@@ -617,6 +738,11 @@
|
|||||||
"series": "Serie",
|
"series": "Serie",
|
||||||
"seasons": "Stagioni",
|
"seasons": "Stagioni",
|
||||||
"season": "Stagione",
|
"season": "Stagione",
|
||||||
|
"from_this_series": "From This Series",
|
||||||
|
"more_from_this_season": "More from this Season",
|
||||||
|
"view_series": "View Series",
|
||||||
|
"view_season": "View Season",
|
||||||
|
"select_season": "Select Season",
|
||||||
"no_episodes_for_this_season": "Nessun episodio per questa stagione",
|
"no_episodes_for_this_season": "Nessun episodio per questa stagione",
|
||||||
"overview": "Panoramica",
|
"overview": "Panoramica",
|
||||||
"more_with": "Altri con {{name}}",
|
"more_with": "Altri con {{name}}",
|
||||||
@@ -627,10 +753,21 @@
|
|||||||
"media_options": "Opzioni Media",
|
"media_options": "Opzioni Media",
|
||||||
"quality": "Qualità",
|
"quality": "Qualità",
|
||||||
"audio": "Audio",
|
"audio": "Audio",
|
||||||
"subtitles": "Sottotitoli",
|
"subtitles": {
|
||||||
|
"label": "Subtitle",
|
||||||
|
"none": "None",
|
||||||
|
"tracks": "Tracks"
|
||||||
|
},
|
||||||
"show_more": "Mostra di più",
|
"show_more": "Mostra di più",
|
||||||
"show_less": "Mostra di meno",
|
"show_less": "Mostra di meno",
|
||||||
|
"left": "left",
|
||||||
|
"more_info": "More Info",
|
||||||
|
"director": "Director",
|
||||||
|
"cast": "Cast",
|
||||||
|
"technical_details": "Technical Details",
|
||||||
"appeared_in": "Apparso in",
|
"appeared_in": "Apparso in",
|
||||||
|
"movies": "Movies",
|
||||||
|
"shows": "Shows",
|
||||||
"could_not_load_item": "Impossibile caricare l'elemento",
|
"could_not_load_item": "Impossibile caricare l'elemento",
|
||||||
"none": "Nessuno",
|
"none": "Nessuno",
|
||||||
"download": {
|
"download": {
|
||||||
@@ -641,7 +778,13 @@
|
|||||||
"download_x_item": "Scarica {{item_count}} elementi",
|
"download_x_item": "Scarica {{item_count}} elementi",
|
||||||
"download_unwatched_only": "Solo Non Visti",
|
"download_unwatched_only": "Solo Non Visti",
|
||||||
"download_button": "Scarica"
|
"download_button": "Scarica"
|
||||||
}
|
},
|
||||||
|
"mark_played": "Mark as Watched",
|
||||||
|
"mark_unplayed": "Mark as Unwatched",
|
||||||
|
"resume_playback": "Resume Playback",
|
||||||
|
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
|
||||||
|
"play_from_start": "Play from Start",
|
||||||
|
"continue_from": "Continue from {{time}}"
|
||||||
},
|
},
|
||||||
"live_tv": {
|
"live_tv": {
|
||||||
"next": "Prossimo",
|
"next": "Prossimo",
|
||||||
@@ -652,7 +795,18 @@
|
|||||||
"movies": "Film",
|
"movies": "Film",
|
||||||
"sports": "Sport",
|
"sports": "Sport",
|
||||||
"for_kids": "Per Bambini",
|
"for_kids": "Per Bambini",
|
||||||
"news": "Notiziari"
|
"news": "Notiziari",
|
||||||
|
"page_of": "Page {{current}} of {{total}}",
|
||||||
|
"no_programs": "No programs available",
|
||||||
|
"no_channels": "No channels available",
|
||||||
|
"tabs": {
|
||||||
|
"programs": "Programs",
|
||||||
|
"guide": "Guide",
|
||||||
|
"channels": "Channels",
|
||||||
|
"recordings": "Recordings",
|
||||||
|
"schedule": "Schedule",
|
||||||
|
"series": "Series"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"jellyseerr": {
|
"jellyseerr": {
|
||||||
"confirm": "Conferma",
|
"confirm": "Conferma",
|
||||||
@@ -697,6 +851,12 @@
|
|||||||
"decline": "Rifiuta",
|
"decline": "Rifiuta",
|
||||||
"requested_by": "Richiesto da {{user}}",
|
"requested_by": "Richiesto da {{user}}",
|
||||||
"unknown_user": "Utente Sconosciuto",
|
"unknown_user": "Utente Sconosciuto",
|
||||||
|
"select": "Select",
|
||||||
|
"request_all": "Request All",
|
||||||
|
"request_seasons": "Request Seasons",
|
||||||
|
"select_seasons": "Select Seasons",
|
||||||
|
"request_selected": "Request Selected",
|
||||||
|
"n_selected": "{{count}} selected",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"jellyseer_does_not_meet_requirements": "Il server Jellyseerr non soddisfa i requisiti minimi di versione! Aggiornare almeno alla versione 2.0.0.",
|
"jellyseer_does_not_meet_requirements": "Il server Jellyseerr non soddisfa i requisiti minimi di versione! Aggiornare almeno alla versione 2.0.0.",
|
||||||
"jellyseerr_test_failed": "Il test di Jellyseerr non è riuscito. Riprovare.",
|
"jellyseerr_test_failed": "Il test di Jellyseerr non è riuscito. Riprovare.",
|
||||||
@@ -716,7 +876,8 @@
|
|||||||
"search": "Cerca",
|
"search": "Cerca",
|
||||||
"library": "Libreria",
|
"library": "Libreria",
|
||||||
"custom_links": "Collegamenti personalizzati",
|
"custom_links": "Collegamenti personalizzati",
|
||||||
"favorites": "Preferiti"
|
"favorites": "Preferiti",
|
||||||
|
"settings": "Settings"
|
||||||
},
|
},
|
||||||
"music": {
|
"music": {
|
||||||
"title": "Music",
|
"title": "Music",
|
||||||
@@ -841,5 +1002,36 @@
|
|||||||
"show": "This show",
|
"show": "This show",
|
||||||
"all": "All media (default)"
|
"all": "All media (default)"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"companion_login": {
|
||||||
|
"title": "Pair with TV",
|
||||||
|
"align_qr": "Align the QR code within the frame",
|
||||||
|
"enter_code_manually": "Enter code manually",
|
||||||
|
"pairing_enter_credentials": "Enter credentials for TV",
|
||||||
|
"pairing_code_label": "Pairing code",
|
||||||
|
"server": "Server",
|
||||||
|
"authorize_button": "Authorize",
|
||||||
|
"authorizing": "Authorizing...",
|
||||||
|
"scan_again": "Scan Again",
|
||||||
|
"done": "Done",
|
||||||
|
"success_title": "Authorization Sent",
|
||||||
|
"pairing_tv_connecting": "The TV is connecting to your account",
|
||||||
|
"error_title": "Authorization Failed",
|
||||||
|
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
|
||||||
|
"error_generic": "Something went wrong. Please try again.",
|
||||||
|
"error_permission_denied": "Camera permission is required to scan QR codes.",
|
||||||
|
"login_as": "Log in as {{username}}?",
|
||||||
|
"on_server": "on {{server}}",
|
||||||
|
"use_different_user": "Use a different user",
|
||||||
|
"open_settings": "Open Settings"
|
||||||
|
},
|
||||||
|
"pairing": {
|
||||||
|
"pair_with_phone": "Pair with Phone",
|
||||||
|
"pair_with_phone_title": "Login TV",
|
||||||
|
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
|
||||||
|
"waiting_for_phone": "Waiting for phone...",
|
||||||
|
"scan_with_phone": "Scan with the Streamyfin app on your phone",
|
||||||
|
"logging_in": "Logging in...",
|
||||||
|
"logging_in_description": "Connecting to your server"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
"error_title": "エラー",
|
"error_title": "エラー",
|
||||||
"login_title": "ログイン",
|
"login_title": "ログイン",
|
||||||
"login_to_title": "ログイン先",
|
"login_to_title": "ログイン先",
|
||||||
|
"select_user": "Select a user to log in",
|
||||||
|
"add_user_to_login": "Add a user to log in",
|
||||||
|
"add_user": "Add User",
|
||||||
"username_placeholder": "ユーザー名",
|
"username_placeholder": "ユーザー名",
|
||||||
"password_placeholder": "パスワード",
|
"password_placeholder": "パスワード",
|
||||||
"login_button": "ログイン",
|
"login_button": "ログイン",
|
||||||
@@ -42,7 +45,13 @@
|
|||||||
"accounts_count": "{{count}} accounts",
|
"accounts_count": "{{count}} accounts",
|
||||||
"select_account": "Select Account",
|
"select_account": "Select Account",
|
||||||
"add_account": "Add Account",
|
"add_account": "Add Account",
|
||||||
"remove_account_description": "This will remove the saved credentials for {{username}}."
|
"remove_account_description": "This will remove the saved credentials for {{username}}.",
|
||||||
|
"remove_server": "Remove Server",
|
||||||
|
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
|
||||||
|
"select_your_server": "Select Your Server",
|
||||||
|
"add_server_to_get_started": "Add a server to get started",
|
||||||
|
"add_server": "Add Server",
|
||||||
|
"change_server": "Change Server"
|
||||||
},
|
},
|
||||||
"save_account": {
|
"save_account": {
|
||||||
"title": "Save Account",
|
"title": "Save Account",
|
||||||
@@ -86,6 +95,7 @@
|
|||||||
"oops": "おっと!",
|
"oops": "おっと!",
|
||||||
"error_message": "何か問題が発生しました。\nログアウトして再度ログインしてください。",
|
"error_message": "何か問題が発生しました。\nログアウトして再度ログインしてください。",
|
||||||
"continue_watching": "続きを見る",
|
"continue_watching": "続きを見る",
|
||||||
|
"continue": "Continue",
|
||||||
"next_up": "次の動画",
|
"next_up": "次の動画",
|
||||||
"continue_and_next_up": "Continue & Next Up",
|
"continue_and_next_up": "Continue & Next Up",
|
||||||
"recently_added_in": "{{libraryName}}に最近追加された",
|
"recently_added_in": "{{libraryName}}に最近追加された",
|
||||||
@@ -109,6 +119,12 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"settings_title": "設定",
|
"settings_title": "設定",
|
||||||
"log_out_button": "ログアウト",
|
"log_out_button": "ログアウト",
|
||||||
|
"switch_user": {
|
||||||
|
"title": "Switch User",
|
||||||
|
"account": "Account",
|
||||||
|
"switch_user": "Switch User on This Server",
|
||||||
|
"current": "current"
|
||||||
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "カテゴリ"
|
"title": "カテゴリ"
|
||||||
},
|
},
|
||||||
@@ -121,7 +137,16 @@
|
|||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "Appearance",
|
"title": "Appearance",
|
||||||
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
||||||
"hide_remote_session_button": "Hide Remote Session Button"
|
"hide_remote_session_button": "Hide Remote Session Button",
|
||||||
|
"show_home_backdrop": "Dynamic Home Backdrop",
|
||||||
|
"show_hero_carousel": "Hero Carousel",
|
||||||
|
"show_series_poster_on_episode": "Show Series Poster on Episodes",
|
||||||
|
"theme_music": "Theme Music",
|
||||||
|
"display_size": "Display Size",
|
||||||
|
"display_size_small": "Small",
|
||||||
|
"display_size_default": "Default",
|
||||||
|
"display_size_large": "Large",
|
||||||
|
"display_size_extra_large": "Extra Large"
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"title": "Network",
|
"title": "Network",
|
||||||
@@ -174,6 +199,22 @@
|
|||||||
"rewind_length": "巻き戻しの長さ",
|
"rewind_length": "巻き戻しの長さ",
|
||||||
"seconds_unit": "s"
|
"seconds_unit": "s"
|
||||||
},
|
},
|
||||||
|
"buffer": {
|
||||||
|
"title": "Buffer Settings",
|
||||||
|
"cache_mode": "Cache Mode",
|
||||||
|
"cache_auto": "Auto",
|
||||||
|
"cache_yes": "Enabled",
|
||||||
|
"cache_no": "Disabled",
|
||||||
|
"buffer_duration": "Buffer Duration",
|
||||||
|
"max_cache_size": "Max Cache Size",
|
||||||
|
"max_backward_cache": "Max Backward Cache"
|
||||||
|
},
|
||||||
|
"vo_driver": {
|
||||||
|
"title": "Video Output",
|
||||||
|
"vo_mode": "VO Driver",
|
||||||
|
"gpu_next": "gpu-next (Recommended)",
|
||||||
|
"gpu": "gpu"
|
||||||
|
},
|
||||||
"gesture_controls": {
|
"gesture_controls": {
|
||||||
"gesture_controls_title": "ジェスチャーコントロール",
|
"gesture_controls_title": "ジェスチャーコントロール",
|
||||||
"horizontal_swipe_skip": "水平方向にスワイプしてスキップ",
|
"horizontal_swipe_skip": "水平方向にスワイプしてスキップ",
|
||||||
@@ -256,7 +297,23 @@
|
|||||||
"subtitle_font": "Subtitle Font",
|
"subtitle_font": "Subtitle Font",
|
||||||
"ksplayer_title": "KSPlayer Settings",
|
"ksplayer_title": "KSPlayer Settings",
|
||||||
"hardware_decode": "Hardware Decoding",
|
"hardware_decode": "Hardware Decoding",
|
||||||
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
|
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.",
|
||||||
|
"opensubtitles_title": "OpenSubtitles",
|
||||||
|
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
|
||||||
|
"opensubtitles_api_key": "API Key",
|
||||||
|
"opensubtitles_api_key_placeholder": "Enter API key...",
|
||||||
|
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
|
||||||
|
"mpv_subtitle_scale": "Subtitle Scale",
|
||||||
|
"mpv_subtitle_margin_y": "Vertical Margin",
|
||||||
|
"mpv_subtitle_align_x": "Horizontal Align",
|
||||||
|
"mpv_subtitle_align_y": "Vertical Align",
|
||||||
|
"align": {
|
||||||
|
"left": "Left",
|
||||||
|
"center": "Center",
|
||||||
|
"right": "Right",
|
||||||
|
"top": "Top",
|
||||||
|
"bottom": "Bottom"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"vlc_subtitles": {
|
"vlc_subtitles": {
|
||||||
"title": "VLC Subtitle Settings",
|
"title": "VLC Subtitle Settings",
|
||||||
@@ -406,7 +463,13 @@
|
|||||||
"music_cache_cleared": "Music cache cleared",
|
"music_cache_cleared": "Music cache cleared",
|
||||||
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
||||||
"downloaded_songs_size": "{{size}} downloaded",
|
"downloaded_songs_size": "{{size}} downloaded",
|
||||||
"downloaded_songs_deleted": "Downloaded songs deleted"
|
"downloaded_songs_deleted": "Downloaded songs deleted",
|
||||||
|
"clear_all_cache": "Clear All Cache",
|
||||||
|
"clear_all_cache_confirm": "Clear All Cache?",
|
||||||
|
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
|
||||||
|
"clear_all_cache_success": "Cache Cleared",
|
||||||
|
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
|
||||||
|
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "イントロ",
|
"title": "イントロ",
|
||||||
@@ -430,6 +493,21 @@
|
|||||||
"error_deleting_files": "ファイルの削除エラー",
|
"error_deleting_files": "ファイルの削除エラー",
|
||||||
"background_downloads_enabled": "バックグラウンドでのダウンロードは有効です",
|
"background_downloads_enabled": "バックグラウンドでのダウンロードは有効です",
|
||||||
"background_downloads_disabled": "バックグラウンドでのダウンロードは無効です"
|
"background_downloads_disabled": "バックグラウンドでのダウンロードは無効です"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"title": "Security",
|
||||||
|
"inactivity_timeout": {
|
||||||
|
"title": "Inactivity Timeout",
|
||||||
|
"description": "Auto logout after inactivity",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"1_minute": "1 minute",
|
||||||
|
"5_minutes": "5 minutes",
|
||||||
|
"15_minutes": "15 minutes",
|
||||||
|
"30_minutes": "30 minutes",
|
||||||
|
"1_hour": "1 hour",
|
||||||
|
"4_hours": "4 hours",
|
||||||
|
"24_hours": "24 hours"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -456,6 +534,7 @@
|
|||||||
"new_app_version_requires_re_download_description": "新しいアップデートではコンテンツを再度ダウンロードする必要があります。ダウンロードしたコンテンツをすべて削除してもう一度お試しください。",
|
"new_app_version_requires_re_download_description": "新しいアップデートではコンテンツを再度ダウンロードする必要があります。ダウンロードしたコンテンツをすべて削除してもう一度お試しください。",
|
||||||
"back": "戻る",
|
"back": "戻る",
|
||||||
"delete": "削除",
|
"delete": "削除",
|
||||||
|
"delete_download": "Delete Download",
|
||||||
"something_went_wrong": "問題が発生しました",
|
"something_went_wrong": "問題が発生しました",
|
||||||
"could_not_get_stream_url_from_jellyfin": "JellyfinからストリームURLを取得できませんでした",
|
"could_not_get_stream_url_from_jellyfin": "JellyfinからストリームURLを取得できませんでした",
|
||||||
"eta": "ETA {{eta}}",
|
"eta": "ETA {{eta}}",
|
||||||
@@ -492,22 +571,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"no_results": "No Results",
|
||||||
"select": "選択",
|
"select": "選択",
|
||||||
"no_trailer_available": "トレーラーがありません",
|
"no_trailer_available": "トレーラーがありません",
|
||||||
"video": "映像",
|
"video": "映像",
|
||||||
"audio": "音声",
|
"audio": "音声",
|
||||||
"subtitle": "字幕",
|
"subtitle": "字幕",
|
||||||
"play": "再生",
|
"play": "再生",
|
||||||
|
"mark_as_played": "Mark as Played",
|
||||||
|
"mark_as_not_played": "Mark as not Played",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
"track": "Track",
|
"track": "Track",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
|
"stop": "Stop",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"verifying": "Verifying..."
|
"verifying": "Verifying...",
|
||||||
|
"login": "Login",
|
||||||
|
"refresh": "Refresh"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"search": "検索...",
|
"search": "検索...",
|
||||||
@@ -556,6 +641,7 @@
|
|||||||
"movies": "映画",
|
"movies": "映画",
|
||||||
"series": "シリーズ",
|
"series": "シリーズ",
|
||||||
"boxsets": "ボックスセット",
|
"boxsets": "ボックスセット",
|
||||||
|
"playlists": "Playlists",
|
||||||
"items": "アイテム"
|
"items": "アイテム"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
@@ -566,7 +652,8 @@
|
|||||||
"poster": "ポスター",
|
"poster": "ポスター",
|
||||||
"cover": "カバー",
|
"cover": "カバー",
|
||||||
"show_titles": "タイトルの表示",
|
"show_titles": "タイトルの表示",
|
||||||
"show_stats": "統計を表示"
|
"show_stats": "統計を表示",
|
||||||
|
"options_title": "Options"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"genres": "ジャンル",
|
"genres": "ジャンル",
|
||||||
@@ -574,7 +661,11 @@
|
|||||||
"sort_by": "ソート",
|
"sort_by": "ソート",
|
||||||
"filter_by": "Filter By",
|
"filter_by": "Filter By",
|
||||||
"sort_order": "ソート順",
|
"sort_order": "ソート順",
|
||||||
"tags": "タグ"
|
"tags": "タグ",
|
||||||
|
"all": "All",
|
||||||
|
"reset": "Reset",
|
||||||
|
"asc": "Ascending",
|
||||||
|
"desc": "Descending"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"favorites": {
|
"favorites": {
|
||||||
@@ -591,6 +682,8 @@
|
|||||||
"no_links": "リンクがありません"
|
"no_links": "リンクがありません"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
|
"live": "LIVE",
|
||||||
|
"mpv_player_title": "MPV Player",
|
||||||
"error": "エラー",
|
"error": "エラー",
|
||||||
"failed_to_get_stream_url": "ストリームURLを取得できませんでした",
|
"failed_to_get_stream_url": "ストリームURLを取得できませんでした",
|
||||||
"an_error_occured_while_playing_the_video": "動画の再生中にエラーが発生しました。設定でログを確認してください。",
|
"an_error_occured_while_playing_the_video": "動画の再生中にエラーが発生しました。設定でログを確認してください。",
|
||||||
@@ -608,7 +701,35 @@
|
|||||||
"downloaded_file_message": "Do you want to play the downloaded file?",
|
"downloaded_file_message": "Do you want to play the downloaded file?",
|
||||||
"downloaded_file_yes": "Yes",
|
"downloaded_file_yes": "Yes",
|
||||||
"downloaded_file_no": "No",
|
"downloaded_file_no": "No",
|
||||||
"downloaded_file_cancel": "Cancel"
|
"downloaded_file_cancel": "Cancel",
|
||||||
|
"swipe_down_settings": "Swipe down for settings",
|
||||||
|
"ends_at": "Ends at {{time}}",
|
||||||
|
"search_subtitles": "Search Subtitles",
|
||||||
|
"subtitle_tracks": "Tracks",
|
||||||
|
"subtitle_search": "Search & Download",
|
||||||
|
"download": "Download",
|
||||||
|
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
|
||||||
|
"using_jellyfin_server": "Using Jellyfin Server",
|
||||||
|
"language": "Language",
|
||||||
|
"results": "Results",
|
||||||
|
"searching": "Searching...",
|
||||||
|
"search_failed": "Search failed",
|
||||||
|
"no_subtitle_provider": "No subtitle provider configured on server",
|
||||||
|
"no_subtitles_found": "No subtitles found",
|
||||||
|
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
|
||||||
|
"settings": "Settings",
|
||||||
|
"skip_intro": "Skip Intro",
|
||||||
|
"skip_credits": "Skip Credits",
|
||||||
|
"stopPlayback": "Stop Playback",
|
||||||
|
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
|
||||||
|
"stopPlayingConfirm": "Are you sure you want to stop playback?",
|
||||||
|
"downloaded": "Downloaded"
|
||||||
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "次",
|
"next_up": "次",
|
||||||
@@ -617,6 +738,11 @@
|
|||||||
"series": "シリーズ",
|
"series": "シリーズ",
|
||||||
"seasons": "シーズン",
|
"seasons": "シーズン",
|
||||||
"season": "シーズン",
|
"season": "シーズン",
|
||||||
|
"from_this_series": "From This Series",
|
||||||
|
"more_from_this_season": "More from this Season",
|
||||||
|
"view_series": "View Series",
|
||||||
|
"view_season": "View Season",
|
||||||
|
"select_season": "Select Season",
|
||||||
"no_episodes_for_this_season": "このシーズンのエピソードはありません",
|
"no_episodes_for_this_season": "このシーズンのエピソードはありません",
|
||||||
"overview": "ストーリー",
|
"overview": "ストーリー",
|
||||||
"more_with": "{{name}}の詳細",
|
"more_with": "{{name}}の詳細",
|
||||||
@@ -627,10 +753,21 @@
|
|||||||
"media_options": "Media Options",
|
"media_options": "Media Options",
|
||||||
"quality": "画質",
|
"quality": "画質",
|
||||||
"audio": "音声",
|
"audio": "音声",
|
||||||
"subtitles": "字幕",
|
"subtitles": {
|
||||||
|
"label": "Subtitle",
|
||||||
|
"none": "None",
|
||||||
|
"tracks": "Tracks"
|
||||||
|
},
|
||||||
"show_more": "もっと見る",
|
"show_more": "もっと見る",
|
||||||
"show_less": "少なく表示",
|
"show_less": "少なく表示",
|
||||||
|
"left": "left",
|
||||||
|
"more_info": "More Info",
|
||||||
|
"director": "Director",
|
||||||
|
"cast": "Cast",
|
||||||
|
"technical_details": "Technical Details",
|
||||||
"appeared_in": "出演作品",
|
"appeared_in": "出演作品",
|
||||||
|
"movies": "Movies",
|
||||||
|
"shows": "Shows",
|
||||||
"could_not_load_item": "アイテムを読み込めませんでした",
|
"could_not_load_item": "アイテムを読み込めませんでした",
|
||||||
"none": "なし",
|
"none": "なし",
|
||||||
"download": {
|
"download": {
|
||||||
@@ -641,7 +778,13 @@
|
|||||||
"download_x_item": "{{item_count}}のアイテムをダウンロード",
|
"download_x_item": "{{item_count}}のアイテムをダウンロード",
|
||||||
"download_unwatched_only": "未視聴のみ",
|
"download_unwatched_only": "未視聴のみ",
|
||||||
"download_button": "ダウンロード"
|
"download_button": "ダウンロード"
|
||||||
}
|
},
|
||||||
|
"mark_played": "Mark as Watched",
|
||||||
|
"mark_unplayed": "Mark as Unwatched",
|
||||||
|
"resume_playback": "Resume Playback",
|
||||||
|
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
|
||||||
|
"play_from_start": "Play from Start",
|
||||||
|
"continue_from": "Continue from {{time}}"
|
||||||
},
|
},
|
||||||
"live_tv": {
|
"live_tv": {
|
||||||
"next": "次",
|
"next": "次",
|
||||||
@@ -652,7 +795,18 @@
|
|||||||
"movies": "映画",
|
"movies": "映画",
|
||||||
"sports": "スポーツ",
|
"sports": "スポーツ",
|
||||||
"for_kids": "子供向け",
|
"for_kids": "子供向け",
|
||||||
"news": "ニュース"
|
"news": "ニュース",
|
||||||
|
"page_of": "Page {{current}} of {{total}}",
|
||||||
|
"no_programs": "No programs available",
|
||||||
|
"no_channels": "No channels available",
|
||||||
|
"tabs": {
|
||||||
|
"programs": "Programs",
|
||||||
|
"guide": "Guide",
|
||||||
|
"channels": "Channels",
|
||||||
|
"recordings": "Recordings",
|
||||||
|
"schedule": "Schedule",
|
||||||
|
"series": "Series"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"jellyseerr": {
|
"jellyseerr": {
|
||||||
"confirm": "確認",
|
"confirm": "確認",
|
||||||
@@ -697,6 +851,12 @@
|
|||||||
"decline": "Decline",
|
"decline": "Decline",
|
||||||
"requested_by": "Requested by {{user}}",
|
"requested_by": "Requested by {{user}}",
|
||||||
"unknown_user": "Unknown User",
|
"unknown_user": "Unknown User",
|
||||||
|
"select": "Select",
|
||||||
|
"request_all": "Request All",
|
||||||
|
"request_seasons": "Request Seasons",
|
||||||
|
"select_seasons": "Select Seasons",
|
||||||
|
"request_selected": "Request Selected",
|
||||||
|
"n_selected": "{{count}} selected",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"jellyseer_does_not_meet_requirements": "Jellyseerrサーバーは最小バージョン要件を満たしていません。少なくとも 2.0.0 に更新してください。",
|
"jellyseer_does_not_meet_requirements": "Jellyseerrサーバーは最小バージョン要件を満たしていません。少なくとも 2.0.0 に更新してください。",
|
||||||
"jellyseerr_test_failed": "Jellyseerrテストに失敗しました。もう一度お試しください。",
|
"jellyseerr_test_failed": "Jellyseerrテストに失敗しました。もう一度お試しください。",
|
||||||
@@ -716,7 +876,8 @@
|
|||||||
"search": "検索",
|
"search": "検索",
|
||||||
"library": "ライブラリ",
|
"library": "ライブラリ",
|
||||||
"custom_links": "カスタムリンク",
|
"custom_links": "カスタムリンク",
|
||||||
"favorites": "お気に入り"
|
"favorites": "お気に入り",
|
||||||
|
"settings": "Settings"
|
||||||
},
|
},
|
||||||
"music": {
|
"music": {
|
||||||
"title": "Music",
|
"title": "Music",
|
||||||
@@ -841,5 +1002,36 @@
|
|||||||
"show": "This show",
|
"show": "This show",
|
||||||
"all": "All media (default)"
|
"all": "All media (default)"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"companion_login": {
|
||||||
|
"title": "Pair with TV",
|
||||||
|
"align_qr": "Align the QR code within the frame",
|
||||||
|
"enter_code_manually": "Enter code manually",
|
||||||
|
"pairing_enter_credentials": "Enter credentials for TV",
|
||||||
|
"pairing_code_label": "Pairing code",
|
||||||
|
"server": "Server",
|
||||||
|
"authorize_button": "Authorize",
|
||||||
|
"authorizing": "Authorizing...",
|
||||||
|
"scan_again": "Scan Again",
|
||||||
|
"done": "Done",
|
||||||
|
"success_title": "Authorization Sent",
|
||||||
|
"pairing_tv_connecting": "The TV is connecting to your account",
|
||||||
|
"error_title": "Authorization Failed",
|
||||||
|
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
|
||||||
|
"error_generic": "Something went wrong. Please try again.",
|
||||||
|
"error_permission_denied": "Camera permission is required to scan QR codes.",
|
||||||
|
"login_as": "Log in as {{username}}?",
|
||||||
|
"on_server": "on {{server}}",
|
||||||
|
"use_different_user": "Use a different user",
|
||||||
|
"open_settings": "Open Settings"
|
||||||
|
},
|
||||||
|
"pairing": {
|
||||||
|
"pair_with_phone": "Pair with Phone",
|
||||||
|
"pair_with_phone_title": "Login TV",
|
||||||
|
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
|
||||||
|
"waiting_for_phone": "Waiting for phone...",
|
||||||
|
"scan_with_phone": "Scan with the Streamyfin app on your phone",
|
||||||
|
"logging_in": "Logging in...",
|
||||||
|
"logging_in_description": "Connecting to your server"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,187 +1,228 @@
|
|||||||
{
|
{
|
||||||
"login": {
|
"login": {
|
||||||
"username_required": "Username Is Required",
|
"username_required": "사용자 이름이 필요합니다",
|
||||||
"error_title": "Error",
|
"error_title": "오류",
|
||||||
"login_title": "Log In",
|
"login_title": "로그인",
|
||||||
"login_to_title": "Log in to",
|
"login_to_title": "다음 서비스에 연결 중",
|
||||||
"username_placeholder": "Username",
|
"select_user": "Select a user to log in",
|
||||||
"password_placeholder": "Password",
|
"add_user_to_login": "Add a user to log in",
|
||||||
"login_button": "Log In",
|
"add_user": "Add User",
|
||||||
"quick_connect": "Quick Connect",
|
"username_placeholder": "사용자 이름",
|
||||||
"enter_code_to_login": "Enter code {{code}} to login",
|
"password_placeholder": "비밀번호",
|
||||||
"failed_to_initiate_quick_connect": "Failed to initiate Quick Connect",
|
"login_button": "로그인",
|
||||||
"got_it": "Got It",
|
"quick_connect": "퀵 커넥트",
|
||||||
"connection_failed": "Connection Failed",
|
"enter_code_to_login": "로그인 하기 위해 코드{{code}}를 입력하세요",
|
||||||
"could_not_connect_to_server": "Could not connect to the server. Please check the URL and your network connection.",
|
"failed_to_initiate_quick_connect": "Quick Connect 연결을 시작하는 데 실패했습니다",
|
||||||
"an_unexpected_error_occured": "An Unexpected Error Occurred",
|
"got_it": "성공",
|
||||||
"change_server": "Change Server",
|
"connection_failed": "연결 실패",
|
||||||
"invalid_username_or_password": "Invalid Username or Password",
|
"could_not_connect_to_server": "서버에 연결되지 않았습니다. URL과 네트워크 상태를 확인하세요.",
|
||||||
"user_does_not_have_permission_to_log_in": "User does not have permission to log in",
|
"an_unexpected_error_occured": "예기치 않은 오류가 발생했습니다",
|
||||||
"server_is_taking_too_long_to_respond_try_again_later": "Server is taking too long to respond, try again later",
|
"change_server": "서버 변경",
|
||||||
"server_received_too_many_requests_try_again_later": "Server received too many requests, try again later.",
|
"invalid_username_or_password": "잘못된 아이디 혹은 비밀번호입니다",
|
||||||
"there_is_a_server_error": "There is a server error",
|
"user_does_not_have_permission_to_log_in": "로그인 하기 위한 권한이 없습니다",
|
||||||
"an_unexpected_error_occured_did_you_enter_the_correct_url": "An unexpected error occurred. Did you enter the server URL correctly?",
|
"server_is_taking_too_long_to_respond_try_again_later": "서버 응답이 너무 느립니다. 나중에 다시 시도하세요",
|
||||||
|
"server_received_too_many_requests_try_again_later": "서버가 너무 많은 요청을 받았습니다. 나중에 다시 시도하세요.",
|
||||||
|
"there_is_a_server_error": "서버 에러",
|
||||||
|
"an_unexpected_error_occured_did_you_enter_the_correct_url": "예기치 않은 오류가 발생했습니다. 서버 URL을 올바르게 입력하셨습니까?",
|
||||||
"too_old_server_text": "Unsupported Jellyfin Server Discovered",
|
"too_old_server_text": "Unsupported Jellyfin Server Discovered",
|
||||||
"too_old_server_description": "Please update Jellyfin to the latest version"
|
"too_old_server_description": "Please update Jellyfin to the latest version"
|
||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"enter_url_to_jellyfin_server": "Enter the URL to your Jellyfin server",
|
"enter_url_to_jellyfin_server": "Enter the URL to your Jellyfin server",
|
||||||
"server_url_placeholder": "http(s)://your-server.com",
|
"server_url_placeholder": "http(s)://your-server.com",
|
||||||
"connect_button": "Connect",
|
"connect_button": "연결",
|
||||||
"previous_servers": "Previous Servers",
|
"previous_servers": "이전 서버",
|
||||||
"clear_button": "Clear all",
|
"clear_button": "모두 지우기",
|
||||||
"swipe_to_remove": "Swipe to remove",
|
"swipe_to_remove": "스와이프해서 지우기",
|
||||||
"search_for_local_servers": "Search for Local Servers",
|
"search_for_local_servers": "로컬 서버 찾기",
|
||||||
"searching": "Searching...",
|
"searching": "찾는 중...",
|
||||||
"servers": "Servers",
|
"servers": "서버",
|
||||||
"saved": "Saved",
|
"saved": "저장됨",
|
||||||
"session_expired": "Session Expired",
|
"session_expired": "세션 만료됨",
|
||||||
"please_login_again": "Your saved session has expired. Please log in again.",
|
"please_login_again": "사용자 세션이 만료되었습니다. 다시 로그인하십시오.",
|
||||||
"remove_saved_login": "Remove Saved Login",
|
"remove_saved_login": "저장된 로그인 정보 삭제",
|
||||||
"remove_saved_login_description": "This will remove your saved credentials for this server. You'll need to enter your username and password again next time.",
|
"remove_saved_login_description": "해당 서버에 저장된 자격 증명이 삭제됩니다. 다음에 접속할 때는 사용자 이름과 비밀번호를 다시 입력해야 합니다.",
|
||||||
"accounts_count": "{{count}} accounts",
|
"accounts_count": "{{count}} 계정",
|
||||||
"select_account": "Select Account",
|
"select_account": "계정 선택",
|
||||||
"add_account": "Add Account",
|
"add_account": "계정 추가",
|
||||||
"remove_account_description": "This will remove the saved credentials for {{username}}."
|
"remove_account_description": "{{username}}에 저장된 자격 증명이 삭제됩니다.",
|
||||||
|
"remove_server": "Remove Server",
|
||||||
|
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
|
||||||
|
"select_your_server": "Select Your Server",
|
||||||
|
"add_server_to_get_started": "Add a server to get started",
|
||||||
|
"add_server": "Add Server",
|
||||||
|
"change_server": "Change Server"
|
||||||
},
|
},
|
||||||
"save_account": {
|
"save_account": {
|
||||||
"title": "Save Account",
|
"title": "계정 저장",
|
||||||
"save_for_later": "Save this account",
|
"save_for_later": "이 계정 저장",
|
||||||
"security_option": "Security Option",
|
"security_option": "보안 설정",
|
||||||
"no_protection": "No protection",
|
"no_protection": "보안 없음",
|
||||||
"no_protection_desc": "Quick login without authentication",
|
"no_protection_desc": "인증 없이 빠른 로그인",
|
||||||
"pin_code": "PIN code",
|
"pin_code": "PIN 코드",
|
||||||
"pin_code_desc": "4-digit PIN required when switching",
|
"pin_code_desc": "전환하려면 4자리 PIN 필요함",
|
||||||
"password": "Re-enter password",
|
"password": "암호 확인",
|
||||||
"password_desc": "Password required when switching",
|
"password_desc": "전환하려면 비밀번호 필요함",
|
||||||
"save_button": "Save",
|
"save_button": "저장",
|
||||||
"cancel_button": "Cancel"
|
"cancel_button": "취소"
|
||||||
},
|
},
|
||||||
"pin": {
|
"pin": {
|
||||||
"enter_pin": "Enter PIN",
|
"enter_pin": "PIN 입력",
|
||||||
"enter_pin_for": "Enter PIN for {{username}}",
|
"enter_pin_for": "{{username}} PIN 입력",
|
||||||
"enter_4_digits": "Enter 4 digits",
|
"enter_4_digits": "4자리 입력",
|
||||||
"invalid_pin": "Invalid PIN",
|
"invalid_pin": "잘못된 PIN",
|
||||||
"setup_pin": "Set Up PIN",
|
"setup_pin": "PIN 설정",
|
||||||
"confirm_pin": "Confirm PIN",
|
"confirm_pin": "PIN 확인",
|
||||||
"pins_dont_match": "PINs don't match",
|
"pins_dont_match": "PIN이 일치하지 않습니다",
|
||||||
"forgot_pin": "Forgot PIN?",
|
"forgot_pin": "PIN을 잊으셨나요?",
|
||||||
"forgot_pin_desc": "Your saved credentials will be removed"
|
"forgot_pin_desc": "저장된 계정 정보가 삭제됩니다"
|
||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"enter_password": "Enter Password",
|
"enter_password": "비밀번호 입력",
|
||||||
"enter_password_for": "Enter password for {{username}}",
|
"enter_password_for": "{{username}}의 비밀번호 입력",
|
||||||
"invalid_password": "Invalid password"
|
"invalid_password": "잘못된 비밀번호"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"checking_server_connection": "Checking server connection...",
|
"checking_server_connection": "서버 연결 체크중...",
|
||||||
"no_internet": "No Internet",
|
"no_internet": "인터넷에 연결되지 않음",
|
||||||
"no_items": "No Items",
|
"no_items": "항목 없음",
|
||||||
"no_internet_message": "No worries, you can still watch\ndownloaded content.",
|
"no_internet_message": "걱정마세요. 다운로드 된 컨텐츠는 여전히 볼 수 있습니다.",
|
||||||
"checking_server_connection_message": "Checking connection to server",
|
"checking_server_connection_message": "Checking connection to server",
|
||||||
"go_to_downloads": "Go to Downloads",
|
"go_to_downloads": "Go to Downloads",
|
||||||
"retry": "Retry",
|
"retry": "재시도",
|
||||||
"server_unreachable": "Server Unreachable",
|
"server_unreachable": "서버에 연결할 수 없음",
|
||||||
"server_unreachable_message": "Could not reach the server.\nPlease check your network connection.",
|
"server_unreachable_message": "서버에 연결할 수 없습니다. 네트워크 상태를 체크하세요.",
|
||||||
"oops": "Oops!",
|
"oops": "이런!",
|
||||||
"error_message": "Something went wrong.\nPlease log out and in again.",
|
"error_message": "문제가 발생했습니다.\n로그아웃 후 다시 로그인해 주세요.",
|
||||||
"continue_watching": "Continue Watching",
|
"continue_watching": "이어서 보기",
|
||||||
"next_up": "Next Up",
|
"continue": "Continue",
|
||||||
"continue_and_next_up": "Continue & Next Up",
|
"next_up": "다음 시청",
|
||||||
"recently_added_in": "Recently Added in {{libraryName}}",
|
"continue_and_next_up": "이어서 보기 & 다음 시청",
|
||||||
"suggested_movies": "Suggested Movies",
|
"recently_added_in": "최근에 추가된 {{libraryName}}",
|
||||||
"suggested_episodes": "Suggested Episodes",
|
"suggested_movies": "추천 영화",
|
||||||
|
"suggested_episodes": "추천 에피소드",
|
||||||
"intro": {
|
"intro": {
|
||||||
"welcome_to_streamyfin": "Welcome to Streamyfin",
|
"welcome_to_streamyfin": "스트리미핀에 오신 것을 환영합니다",
|
||||||
"a_free_and_open_source_client_for_jellyfin": "A Free and Open-Source Client for Jellyfin.",
|
"a_free_and_open_source_client_for_jellyfin": "젤리핀을 위한 무료 오픈소스 클라이언트입니다.",
|
||||||
"features_title": "Features",
|
"features_title": "기능",
|
||||||
"features_description": "Streamyfin has a bunch of features and integrates with a wide array of software which you can find in the settings menu, these include:",
|
"features_description": "스트리미핀은 다양한 기능을 제공하며 설정 메뉴에서 확인할 수 있는 여러 소프트웨어와 통합됩니다. 이러한 소프트웨어에는 다음이 포함됩니다:",
|
||||||
"jellyseerr_feature_description": "Connect to your Seerr instance and request movies directly in the app.",
|
"jellyseerr_feature_description": "Seerr 인스턴스에 연결하여 앱에서 직접 영화를 요청할 수 있습니다.",
|
||||||
"downloads_feature_title": "Downloads",
|
"downloads_feature_title": "다운로드된 컨텐츠",
|
||||||
"downloads_feature_description": "Download movies and tv-shows to view offline. Use either the default method or install the optimize server to download files in the background.",
|
"downloads_feature_description": "오프라인으로 보기위해 다운로드 하세요. 기본 다운로드 방식을 사용하거나, 백그라운드에서 파일을 다운로드하는 최적화 서버를 설치할 수 있습니다.",
|
||||||
"chromecast_feature_description": "Cast movies and tv-shows to your Chromecast devices.",
|
"chromecast_feature_description": "영화와 TV 프로그램을 Chromecast 기기로 전송하기",
|
||||||
"centralised_settings_plugin_title": "Centralised Settings Plugin",
|
"centralised_settings_plugin_title": "중앙 설정 플러그인",
|
||||||
"centralised_settings_plugin_description": "Configure settings from a centralised location on your Jellyfin server. All client settings for all users will be synced automatically.",
|
"centralised_settings_plugin_description": "Jellyfin 서버의 중앙 집중식 위치에서 설정을 구성합니다. 모든 사용자의 모든 클라이언트 설정이 자동으로 동기화됩니다.",
|
||||||
"done_button": "Done",
|
"done_button": "확인",
|
||||||
"go_to_settings_button": "Go to Settings",
|
"go_to_settings_button": "설정으로 이동",
|
||||||
"read_more": "Read More"
|
"read_more": "자세히 보기"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"settings_title": "Settings",
|
"settings_title": "설정",
|
||||||
"log_out_button": "Log Out",
|
"log_out_button": "로그아웃",
|
||||||
|
"switch_user": {
|
||||||
|
"title": "Switch User",
|
||||||
|
"account": "Account",
|
||||||
|
"switch_user": "Switch User on This Server",
|
||||||
|
"current": "current"
|
||||||
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "Categories"
|
"title": "카테고리"
|
||||||
},
|
},
|
||||||
"playback_controls": {
|
"playback_controls": {
|
||||||
"title": "Playback & Controls"
|
"title": "재생 & 컨트롤"
|
||||||
},
|
},
|
||||||
"audio_subtitles": {
|
"audio_subtitles": {
|
||||||
"title": "Audio & Subtitles"
|
"title": "오디오 & 자막"
|
||||||
},
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "Appearance",
|
"title": "화면 스타일",
|
||||||
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
"merge_next_up_continue_watching": "[이어보기]와 [다음 보기] 합치기",
|
||||||
"hide_remote_session_button": "Hide Remote Session Button"
|
"hide_remote_session_button": "원격 세션 버튼 숨기기",
|
||||||
|
"show_home_backdrop": "Dynamic Home Backdrop",
|
||||||
|
"show_hero_carousel": "Hero Carousel",
|
||||||
|
"show_series_poster_on_episode": "Show Series Poster on Episodes",
|
||||||
|
"theme_music": "Theme Music",
|
||||||
|
"display_size": "Display Size",
|
||||||
|
"display_size_small": "Small",
|
||||||
|
"display_size_default": "Default",
|
||||||
|
"display_size_large": "Large",
|
||||||
|
"display_size_extra_large": "Extra Large"
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"title": "Network",
|
"title": "네트워크",
|
||||||
"local_network": "Local Network",
|
"local_network": "로컬 네트워크",
|
||||||
"auto_switch_enabled": "Auto-switch when at home",
|
"auto_switch_enabled": "홈 네트워크 자동 전환",
|
||||||
"auto_switch_description": "Automatically switch to local URL when connected to home WiFi",
|
"auto_switch_description": "홈 WiFi에 연결되었을 때 로컬 URL로 자동 전환",
|
||||||
"local_url": "Local URL",
|
"local_url": "로컬 URL",
|
||||||
"local_url_hint": "Enter your local server address (e.g., http://192.168.1.100:8096)",
|
"local_url_hint": "로컬 서버 주소를 입력하세요 (e.g., http://192.168.1.100:8096)",
|
||||||
"local_url_placeholder": "http://192.168.1.100:8096",
|
"local_url_placeholder": "http://192.168.1.100:8096",
|
||||||
"home_wifi_networks": "Home WiFi Networks",
|
"home_wifi_networks": "홈 WiFi 네트워크",
|
||||||
"add_current_network": "Add \"{{ssid}}\"",
|
"add_current_network": "\"{{ssid}}\" 추가",
|
||||||
"not_connected_to_wifi": "Not connected to WiFi",
|
"not_connected_to_wifi": "WiFi에 연결되지 않음",
|
||||||
"no_networks_configured": "No networks configured",
|
"no_networks_configured": "구성된 네트워크가 없습니다",
|
||||||
"add_network_hint": "Add your home WiFi network to enable auto-switching",
|
"add_network_hint": "자동 전환을 위한 홈 WiFi 추가",
|
||||||
"current_wifi": "Current WiFi",
|
"current_wifi": "현재 WiFi",
|
||||||
"using_url": "Using",
|
"using_url": "사용중",
|
||||||
"local": "Local URL",
|
"local": "로컬 URL",
|
||||||
"remote": "Remote URL",
|
"remote": "원격 URL",
|
||||||
"not_connected": "Not connected",
|
"not_connected": "연결되지 않았습니다",
|
||||||
"current_server": "Current Server",
|
"current_server": "현재 서버",
|
||||||
"remote_url": "Remote URL",
|
"remote_url": "원격 URL",
|
||||||
"active_url": "Active URL",
|
"active_url": "현재 사용 중인 URL",
|
||||||
"not_configured": "Not configured",
|
"not_configured": "설정되지 않음",
|
||||||
"network_added": "Network added",
|
"network_added": "네트워크 추가됨",
|
||||||
"network_already_added": "Network already added",
|
"network_already_added": "네트워크 이미 추가됨",
|
||||||
"no_wifi_connected": "Not connected to WiFi",
|
"no_wifi_connected": "WiFi에 연결되지 않음",
|
||||||
"permission_denied": "Location permission denied",
|
"permission_denied": "위치 권한이 거부되었습니다",
|
||||||
"permission_denied_explanation": "Location permission is required to detect WiFi network for auto-switching. Please enable it in Settings."
|
"permission_denied_explanation": "자동 전환 Wi-Fi 네트워크를 감지하려면 위치 권한이 필요합니다. 설정에서 위치 권한을 활성화해 주세요."
|
||||||
},
|
},
|
||||||
"user_info": {
|
"user_info": {
|
||||||
"user_info_title": "User Info",
|
"user_info_title": "사용자 정보",
|
||||||
"user": "User",
|
"user": "사용자",
|
||||||
"server": "Server",
|
"server": "서버",
|
||||||
"token": "Token",
|
"token": "토큰",
|
||||||
"app_version": "App Version"
|
"app_version": "앱 버전"
|
||||||
},
|
},
|
||||||
"quick_connect": {
|
"quick_connect": {
|
||||||
"quick_connect_title": "Quick Connect",
|
"quick_connect_title": "퀵 커넥트",
|
||||||
"authorize_button": "Authorize Quick Connect",
|
"authorize_button": "퀵 커넥트 승인",
|
||||||
"enter_the_quick_connect_code": "Enter the quick connect code...",
|
"enter_the_quick_connect_code": "퀵 커넥트 코드 입력...",
|
||||||
"success": "Success",
|
"success": "성공",
|
||||||
"quick_connect_autorized": "Quick Connect Authorized",
|
"quick_connect_autorized": "퀵 커넥트 승인됨",
|
||||||
"error": "Error",
|
"error": "오류",
|
||||||
"invalid_code": "Invalid Code",
|
"invalid_code": "유효하지 않은 코드",
|
||||||
"authorize": "Authorize"
|
"authorize": "승인"
|
||||||
},
|
},
|
||||||
"media_controls": {
|
"media_controls": {
|
||||||
"media_controls_title": "Media Controls",
|
"media_controls_title": "미디어 컨트롤",
|
||||||
"forward_skip_length": "Forward Skip Length",
|
"forward_skip_length": "앞으로 건너뛸 시간",
|
||||||
"rewind_length": "Rewind Length",
|
"rewind_length": "뒤로 되감을 시간",
|
||||||
"seconds_unit": "s"
|
"seconds_unit": "초"
|
||||||
|
},
|
||||||
|
"buffer": {
|
||||||
|
"title": "Buffer Settings",
|
||||||
|
"cache_mode": "Cache Mode",
|
||||||
|
"cache_auto": "Auto",
|
||||||
|
"cache_yes": "Enabled",
|
||||||
|
"cache_no": "Disabled",
|
||||||
|
"buffer_duration": "Buffer Duration",
|
||||||
|
"max_cache_size": "Max Cache Size",
|
||||||
|
"max_backward_cache": "Max Backward Cache"
|
||||||
|
},
|
||||||
|
"vo_driver": {
|
||||||
|
"title": "Video Output",
|
||||||
|
"vo_mode": "VO Driver",
|
||||||
|
"gpu_next": "gpu-next (Recommended)",
|
||||||
|
"gpu": "gpu"
|
||||||
},
|
},
|
||||||
"gesture_controls": {
|
"gesture_controls": {
|
||||||
"gesture_controls_title": "Gesture Controls",
|
"gesture_controls_title": "제스처 제어",
|
||||||
"horizontal_swipe_skip": "Horizontal Swipe to Skip",
|
"horizontal_swipe_skip": "좌/우로 스와이프하여 건너뛰기",
|
||||||
"horizontal_swipe_skip_description": "Swipe left/right when controls are hidden to skip",
|
"horizontal_swipe_skip_description": "컨트롤 숨김상태에서 좌/우로 스와이프하여 건너뛰기",
|
||||||
"left_side_brightness": "Left Side Brightness Control",
|
"left_side_brightness": "왼쪽 영역 밝기 조정 컨트롤",
|
||||||
"left_side_brightness_description": "Swipe up/down on left side to adjust brightness",
|
"left_side_brightness_description": "왼쪽 영역을 위/아래 스와이프하여 밝기 조절",
|
||||||
"right_side_volume": "Right Side Volume Control",
|
"right_side_volume": "오른쪽 영역 볼륨 컨트롤",
|
||||||
"right_side_volume_description": "Swipe up/down on right side to adjust volume",
|
"right_side_volume_description": "오른족 영역을 위/아래로 스와이프 하여 볼륨 조절",
|
||||||
"hide_volume_slider": "Hide Volume Slider",
|
"hide_volume_slider": "Hide Volume Slider",
|
||||||
"hide_volume_slider_description": "Hide the volume slider in the video player",
|
"hide_volume_slider_description": "Hide the volume slider in the video player",
|
||||||
"hide_brightness_slider": "Hide Brightness Slider",
|
"hide_brightness_slider": "Hide Brightness Slider",
|
||||||
@@ -196,7 +237,7 @@
|
|||||||
"language": "Language",
|
"language": "Language",
|
||||||
"transcode_mode": {
|
"transcode_mode": {
|
||||||
"title": "Audio Transcoding",
|
"title": "Audio Transcoding",
|
||||||
"description": "Controls how surround audio (7.1, TrueHD, DTS-HD) is handled",
|
"description": "서라운드 오디오(7.1, TrueHD, DTS-HD)를 어떻게 처리할지 설정합니다",
|
||||||
"auto": "Auto",
|
"auto": "Auto",
|
||||||
"stereo": "Force Stereo",
|
"stereo": "Force Stereo",
|
||||||
"5_1": "Allow 5.1",
|
"5_1": "Allow 5.1",
|
||||||
@@ -228,52 +269,68 @@
|
|||||||
"outline_opacity": "Outline Opacity",
|
"outline_opacity": "Outline Opacity",
|
||||||
"bold_text": "Bold Text",
|
"bold_text": "Bold Text",
|
||||||
"colors": {
|
"colors": {
|
||||||
"Black": "Black",
|
"Black": "검정색",
|
||||||
"Gray": "Gray",
|
"Gray": "회색",
|
||||||
"Silver": "Silver",
|
"Silver": "은색",
|
||||||
"White": "White",
|
"White": "흰색",
|
||||||
"Maroon": "Maroon",
|
"Maroon": "밤색",
|
||||||
"Red": "Red",
|
"Red": "빨간색",
|
||||||
"Fuchsia": "Fuchsia",
|
"Fuchsia": "분홍색",
|
||||||
"Yellow": "Yellow",
|
"Yellow": "노란색",
|
||||||
"Olive": "Olive",
|
"Olive": "올리브 색",
|
||||||
"Green": "Green",
|
"Green": "녹색",
|
||||||
"Teal": "Teal",
|
"Teal": "청록색",
|
||||||
"Lime": "Lime",
|
"Lime": "라임색",
|
||||||
"Purple": "Purple",
|
"Purple": "보라색",
|
||||||
"Navy": "Navy",
|
"Navy": "남색",
|
||||||
"Blue": "Blue",
|
"Blue": "파란색",
|
||||||
"Aqua": "Aqua"
|
"Aqua": "아쿠아색"
|
||||||
},
|
},
|
||||||
"thickness": {
|
"thickness": {
|
||||||
"None": "None",
|
"None": "없음",
|
||||||
"Thin": "Thin",
|
"Thin": "얇게",
|
||||||
"Normal": "Normal",
|
"Normal": "보통",
|
||||||
"Thick": "Thick"
|
"Thick": "굵게"
|
||||||
},
|
},
|
||||||
"subtitle_color": "Subtitle Color",
|
"subtitle_color": "자막 색상",
|
||||||
"subtitle_background_color": "Background Color",
|
"subtitle_background_color": "배경 색상",
|
||||||
"subtitle_font": "Subtitle Font",
|
"subtitle_font": "자막 폰트",
|
||||||
"ksplayer_title": "KSPlayer Settings",
|
"ksplayer_title": "KSPlayer 설정",
|
||||||
"hardware_decode": "Hardware Decoding",
|
"hardware_decode": "하드웨어 디코딩",
|
||||||
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
|
"hardware_decode_description": "비디오 디코딩에 하드웨어 가속을 사용하십시오. 재생 문제가 발생하는 경우 비활성화하십시오.",
|
||||||
|
"opensubtitles_title": "OpenSubtitles",
|
||||||
|
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
|
||||||
|
"opensubtitles_api_key": "API Key",
|
||||||
|
"opensubtitles_api_key_placeholder": "Enter API key...",
|
||||||
|
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
|
||||||
|
"mpv_subtitle_scale": "Subtitle Scale",
|
||||||
|
"mpv_subtitle_margin_y": "Vertical Margin",
|
||||||
|
"mpv_subtitle_align_x": "Horizontal Align",
|
||||||
|
"mpv_subtitle_align_y": "Vertical Align",
|
||||||
|
"align": {
|
||||||
|
"left": "Left",
|
||||||
|
"center": "Center",
|
||||||
|
"right": "Right",
|
||||||
|
"top": "Top",
|
||||||
|
"bottom": "Bottom"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"vlc_subtitles": {
|
"vlc_subtitles": {
|
||||||
"title": "VLC Subtitle Settings",
|
"title": "VLC 자막 설정",
|
||||||
"hint": "Customize subtitle appearance for VLC player. Changes take effect on next playback.",
|
"hint": "VLC 플레이어의 자막 표시 방식을 설정하세요. 변경 사항은 다음 재생 시 적용됩니다.",
|
||||||
"text_color": "Text Color",
|
"text_color": "글자색",
|
||||||
"background_color": "Background Color",
|
"background_color": "배경 색상",
|
||||||
"background_opacity": "Background Opacity",
|
"background_opacity": "배경 투명도",
|
||||||
"outline_color": "Outline Color",
|
"outline_color": "외곽선 색상",
|
||||||
"outline_opacity": "Outline Opacity",
|
"outline_opacity": "외곽선 투명도",
|
||||||
"outline_thickness": "Outline Thickness",
|
"outline_thickness": "외곽선 굵기",
|
||||||
"bold": "Bold Text",
|
"bold": "굵은 글씨",
|
||||||
"margin": "Bottom Margin"
|
"margin": "아래쪽 여백"
|
||||||
},
|
},
|
||||||
"video_player": {
|
"video_player": {
|
||||||
"title": "Video Player",
|
"title": "비디오 플레이어",
|
||||||
"video_player": "Video Player",
|
"video_player": "비디오 플레이어",
|
||||||
"video_player_description": "Choose which video player to use on iOS.",
|
"video_player_description": "iOS 사용자는 비디오 플레이어를 선택하세요.",
|
||||||
"ksplayer": "KSPlayer",
|
"ksplayer": "KSPlayer",
|
||||||
"vlc": "VLC"
|
"vlc": "VLC"
|
||||||
},
|
},
|
||||||
@@ -288,20 +345,20 @@
|
|||||||
"PORTRAIT_UP": "Portrait Up",
|
"PORTRAIT_UP": "Portrait Up",
|
||||||
"PORTRAIT_DOWN": "Portrait Down",
|
"PORTRAIT_DOWN": "Portrait Down",
|
||||||
"LANDSCAPE": "Landscape",
|
"LANDSCAPE": "Landscape",
|
||||||
"LANDSCAPE_LEFT": "Landscape Left",
|
"LANDSCAPE_LEFT": "왼쪽 가로 모드",
|
||||||
"LANDSCAPE_RIGHT": "Landscape Right",
|
"LANDSCAPE_RIGHT": "오른쪽 가로 모드",
|
||||||
"OTHER": "Other",
|
"OTHER": "Other",
|
||||||
"UNKNOWN": "Unknown"
|
"UNKNOWN": "Unknown"
|
||||||
},
|
},
|
||||||
"safe_area_in_controls": "Safe Area in Controls",
|
"safe_area_in_controls": "컨트롤 안전 영역",
|
||||||
"video_player": "Video Player",
|
"video_player": "Video Player",
|
||||||
"video_players": {
|
"video_players": {
|
||||||
"VLC_3": "VLC 3",
|
"VLC_3": "VLC 3",
|
||||||
"VLC_4": "VLC 4 (Experimental + PiP)"
|
"VLC_4": "VLC 4 (Experimental + PiP)"
|
||||||
},
|
},
|
||||||
"show_custom_menu_links": "Show Custom Menu Links",
|
"show_custom_menu_links": "사용자 지정 메뉴 링크 표시",
|
||||||
"show_large_home_carousel": "Show Large Home Carousel (beta)",
|
"show_large_home_carousel": "대형 홈 슬라이드 배너 표시 (베타)",
|
||||||
"hide_libraries": "Hide Libraries",
|
"hide_libraries": "라이브러리 숨기기",
|
||||||
"select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.",
|
"select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.",
|
||||||
"disable_haptic_feedback": "Disable Haptic Feedback",
|
"disable_haptic_feedback": "Disable Haptic Feedback",
|
||||||
"default_quality": "Default Quality",
|
"default_quality": "Default Quality",
|
||||||
@@ -334,24 +391,24 @@
|
|||||||
"password": "Password",
|
"password": "Password",
|
||||||
"password_placeholder": "Enter password for Jellyfin user {{username}}",
|
"password_placeholder": "Enter password for Jellyfin user {{username}}",
|
||||||
"login_button": "Login",
|
"login_button": "Login",
|
||||||
"total_media_requests": "Total Media Requests",
|
"total_media_requests": "전체 미디어 요청 수",
|
||||||
"movie_quota_limit": "Movie Quota Limit",
|
"movie_quota_limit": "영화 요청 한도",
|
||||||
"movie_quota_days": "Movie Quota Days",
|
"movie_quota_days": "영화 요청 제한 기간",
|
||||||
"tv_quota_limit": "TV Quota Limit",
|
"tv_quota_limit": "TV Quota Limit",
|
||||||
"tv_quota_days": "TV Quota Days",
|
"tv_quota_days": "TV 요청 제한 기간",
|
||||||
"reset_jellyseerr_config_button": "Reset Seerr Config",
|
"reset_jellyseerr_config_button": "Seerr 설정 초기화",
|
||||||
"unlimited": "Unlimited",
|
"unlimited": "Unlimited",
|
||||||
"plus_n_more": "+{{n}} More",
|
"plus_n_more": "+{{n}}개 더",
|
||||||
"order_by": {
|
"order_by": {
|
||||||
"DEFAULT": "Default",
|
"DEFAULT": "Default",
|
||||||
"VOTE_COUNT_AND_AVERAGE": "Vote count and average",
|
"VOTE_COUNT_AND_AVERAGE": "평균 평점 및 투표 수",
|
||||||
"POPULARITY": "Popularity"
|
"POPULARITY": "Popularity"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"marlin_search": {
|
"marlin_search": {
|
||||||
"enable_marlin_search": "Enable Marlin Search",
|
"enable_marlin_search": "Marlin 검색 활성화",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
"server_url_placeholder": "http(s)://domain.org:port",
|
"server_url_placeholder": "http(s)://도메인:포트",
|
||||||
"marlin_search_hint": "Enter the URL for the Marlin server. The URL should include http or https and optionally the port.",
|
"marlin_search_hint": "Enter the URL for the Marlin server. The URL should include http or https and optionally the port.",
|
||||||
"read_more_about_marlin": "Read More About Marlin.",
|
"read_more_about_marlin": "Read More About Marlin.",
|
||||||
"save_button": "Save",
|
"save_button": "Save",
|
||||||
@@ -374,28 +431,28 @@
|
|||||||
"features_title": "Features",
|
"features_title": "Features",
|
||||||
"home_sections_title": "Home Sections",
|
"home_sections_title": "Home Sections",
|
||||||
"enable_movie_recommendations": "Movie Recommendations",
|
"enable_movie_recommendations": "Movie Recommendations",
|
||||||
"enable_series_recommendations": "Series Recommendations",
|
"enable_series_recommendations": "시리즈 추천",
|
||||||
"enable_promoted_watchlists": "Promoted Watchlists",
|
"enable_promoted_watchlists": "추천 관심 목록",
|
||||||
"hide_watchlists_tab": "Hide Watchlists Tab",
|
"hide_watchlists_tab": "관심 목록 탭 숨기기",
|
||||||
"home_sections_hint": "Show personalized recommendations and promoted watchlists from Streamystats on the home page.",
|
"home_sections_hint": "홈 페이지에서 Streamystats의 개인 맞춤 추천 및 추천 관심 목록을 표시합니다.",
|
||||||
"recommended_movies": "Recommended Movies",
|
"recommended_movies": "Recommended Movies",
|
||||||
"recommended_series": "Recommended Series",
|
"recommended_series": "추천 시리즈",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"saved": "Saved",
|
"saved": "Saved",
|
||||||
"refreshed": "Settings refreshed from server",
|
"refreshed": "서버에서 설정을 새로고침했습니다",
|
||||||
"disabled": "Streamystats disabled"
|
"disabled": "Streamystats 비활성화됨"
|
||||||
},
|
},
|
||||||
"refresh_from_server": "Refresh Settings from Server"
|
"refresh_from_server": "서버에서 설정 새로고침"
|
||||||
},
|
},
|
||||||
"kefinTweaks": {
|
"kefinTweaks": {
|
||||||
"watchlist_enabler": "Enable our Watchlist integration",
|
"watchlist_enabler": "관심 목록 통합 기능 활성화",
|
||||||
"watchlist_button": "Toggle Watchlist integration"
|
"watchlist_button": "관심 목록 연동 켜기/끄기"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"storage": {
|
"storage": {
|
||||||
"storage_title": "Storage",
|
"storage_title": "Storage",
|
||||||
"app_usage": "App {{usedSpace}}%",
|
"app_usage": "앱 {{usedSpace}}",
|
||||||
"device_usage": "Device {{availableSpace}}%",
|
"device_usage": "디바이스 {{availableSpace}}%",
|
||||||
"size_used": "{{used}} of {{total}} Used",
|
"size_used": "{{used}} of {{total}} Used",
|
||||||
"delete_all_downloaded_files": "Delete All Downloaded Files",
|
"delete_all_downloaded_files": "Delete All Downloaded Files",
|
||||||
"music_cache_title": "Music Cache",
|
"music_cache_title": "Music Cache",
|
||||||
@@ -403,10 +460,16 @@
|
|||||||
"enable_music_cache": "Enable Music Cache",
|
"enable_music_cache": "Enable Music Cache",
|
||||||
"clear_music_cache": "Clear Music Cache",
|
"clear_music_cache": "Clear Music Cache",
|
||||||
"music_cache_size": "{{size}} cached",
|
"music_cache_size": "{{size}} cached",
|
||||||
"music_cache_cleared": "Music cache cleared",
|
"music_cache_cleared": "음악 캐시가 삭제되었습니다",
|
||||||
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
||||||
"downloaded_songs_size": "{{size}} downloaded",
|
"downloaded_songs_size": "{{size}} downloaded",
|
||||||
"downloaded_songs_deleted": "Downloaded songs deleted"
|
"downloaded_songs_deleted": "다운로드한 노래가 삭제되었습니다",
|
||||||
|
"clear_all_cache": "Clear All Cache",
|
||||||
|
"clear_all_cache_confirm": "Clear All Cache?",
|
||||||
|
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
|
||||||
|
"clear_all_cache_success": "Cache Cleared",
|
||||||
|
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
|
||||||
|
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "Intro",
|
"title": "Intro",
|
||||||
@@ -430,11 +493,26 @@
|
|||||||
"error_deleting_files": "Error Deleting Files",
|
"error_deleting_files": "Error Deleting Files",
|
||||||
"background_downloads_enabled": "Background downloads enabled",
|
"background_downloads_enabled": "Background downloads enabled",
|
||||||
"background_downloads_disabled": "Background downloads disabled"
|
"background_downloads_disabled": "Background downloads disabled"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"title": "Security",
|
||||||
|
"inactivity_timeout": {
|
||||||
|
"title": "Inactivity Timeout",
|
||||||
|
"description": "Auto logout after inactivity",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"1_minute": "1 minute",
|
||||||
|
"5_minutes": "5 minutes",
|
||||||
|
"15_minutes": "15 minutes",
|
||||||
|
"30_minutes": "30 minutes",
|
||||||
|
"1_hour": "1 hour",
|
||||||
|
"4_hours": "4 hours",
|
||||||
|
"24_hours": "24 hours"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
"title": "Sessions",
|
"title": "Sessions",
|
||||||
"no_active_sessions": "No Active Sessions"
|
"no_active_sessions": "세션 비활성화"
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Downloads",
|
"downloads_title": "Downloads",
|
||||||
@@ -456,6 +534,7 @@
|
|||||||
"new_app_version_requires_re_download_description": "The new update requires content to be downloaded again. Please remove all downloaded content and try again.",
|
"new_app_version_requires_re_download_description": "The new update requires content to be downloaded again. Please remove all downloaded content and try again.",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
|
"delete_download": "Delete Download",
|
||||||
"something_went_wrong": "Something Went Wrong",
|
"something_went_wrong": "Something Went Wrong",
|
||||||
"could_not_get_stream_url_from_jellyfin": "Could not get the stream URL from Jellyfin",
|
"could_not_get_stream_url_from_jellyfin": "Could not get the stream URL from Jellyfin",
|
||||||
"eta": "ETA {{eta}}",
|
"eta": "ETA {{eta}}",
|
||||||
@@ -492,22 +571,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"no_results": "No Results",
|
||||||
"select": "Select",
|
"select": "Select",
|
||||||
"no_trailer_available": "No trailer available",
|
"no_trailer_available": "No trailer available",
|
||||||
"video": "Video",
|
"video": "Video",
|
||||||
"audio": "Audio",
|
"audio": "Audio",
|
||||||
"subtitle": "Subtitle",
|
"subtitle": "Subtitle",
|
||||||
"play": "Play",
|
"play": "Play",
|
||||||
|
"mark_as_played": "Mark as Played",
|
||||||
|
"mark_as_not_played": "Mark as not Played",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
"track": "Track",
|
"track": "Track",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
|
"stop": "Stop",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"verifying": "Verifying..."
|
"verifying": "Verifying...",
|
||||||
|
"login": "Login",
|
||||||
|
"refresh": "Refresh"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"search": "Search...",
|
"search": "Search...",
|
||||||
@@ -556,6 +641,7 @@
|
|||||||
"movies": "Movies",
|
"movies": "Movies",
|
||||||
"series": "Series",
|
"series": "Series",
|
||||||
"boxsets": "Box Sets",
|
"boxsets": "Box Sets",
|
||||||
|
"playlists": "Playlists",
|
||||||
"items": "Items"
|
"items": "Items"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
@@ -566,7 +652,8 @@
|
|||||||
"poster": "Poster",
|
"poster": "Poster",
|
||||||
"cover": "Cover",
|
"cover": "Cover",
|
||||||
"show_titles": "Show Titles",
|
"show_titles": "Show Titles",
|
||||||
"show_stats": "Show Stats"
|
"show_stats": "Show Stats",
|
||||||
|
"options_title": "Options"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"genres": "Genres",
|
"genres": "Genres",
|
||||||
@@ -574,7 +661,11 @@
|
|||||||
"sort_by": "Sort By",
|
"sort_by": "Sort By",
|
||||||
"filter_by": "Filter By",
|
"filter_by": "Filter By",
|
||||||
"sort_order": "Sort Order",
|
"sort_order": "Sort Order",
|
||||||
"tags": "Tags"
|
"tags": "Tags",
|
||||||
|
"all": "All",
|
||||||
|
"reset": "Reset",
|
||||||
|
"asc": "Ascending",
|
||||||
|
"desc": "Descending"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"favorites": {
|
"favorites": {
|
||||||
@@ -591,6 +682,8 @@
|
|||||||
"no_links": "No Links"
|
"no_links": "No Links"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
|
"live": "LIVE",
|
||||||
|
"mpv_player_title": "MPV Player",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"failed_to_get_stream_url": "Failed to get the stream URL",
|
"failed_to_get_stream_url": "Failed to get the stream URL",
|
||||||
"an_error_occured_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.",
|
"an_error_occured_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.",
|
||||||
@@ -608,7 +701,35 @@
|
|||||||
"downloaded_file_message": "Do you want to play the downloaded file?",
|
"downloaded_file_message": "Do you want to play the downloaded file?",
|
||||||
"downloaded_file_yes": "Yes",
|
"downloaded_file_yes": "Yes",
|
||||||
"downloaded_file_no": "No",
|
"downloaded_file_no": "No",
|
||||||
"downloaded_file_cancel": "Cancel"
|
"downloaded_file_cancel": "Cancel",
|
||||||
|
"swipe_down_settings": "Swipe down for settings",
|
||||||
|
"ends_at": "Ends at {{time}}",
|
||||||
|
"search_subtitles": "Search Subtitles",
|
||||||
|
"subtitle_tracks": "Tracks",
|
||||||
|
"subtitle_search": "Search & Download",
|
||||||
|
"download": "Download",
|
||||||
|
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
|
||||||
|
"using_jellyfin_server": "Using Jellyfin Server",
|
||||||
|
"language": "Language",
|
||||||
|
"results": "Results",
|
||||||
|
"searching": "Searching...",
|
||||||
|
"search_failed": "Search failed",
|
||||||
|
"no_subtitle_provider": "No subtitle provider configured on server",
|
||||||
|
"no_subtitles_found": "No subtitles found",
|
||||||
|
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
|
||||||
|
"settings": "Settings",
|
||||||
|
"skip_intro": "Skip Intro",
|
||||||
|
"skip_credits": "Skip Credits",
|
||||||
|
"stopPlayback": "Stop Playback",
|
||||||
|
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
|
||||||
|
"stopPlayingConfirm": "Are you sure you want to stop playback?",
|
||||||
|
"downloaded": "Downloaded"
|
||||||
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Next Up",
|
"next_up": "Next Up",
|
||||||
@@ -617,6 +738,11 @@
|
|||||||
"series": "Series",
|
"series": "Series",
|
||||||
"seasons": "Seasons",
|
"seasons": "Seasons",
|
||||||
"season": "Season",
|
"season": "Season",
|
||||||
|
"from_this_series": "From This Series",
|
||||||
|
"more_from_this_season": "More from this Season",
|
||||||
|
"view_series": "View Series",
|
||||||
|
"view_season": "View Season",
|
||||||
|
"select_season": "Select Season",
|
||||||
"no_episodes_for_this_season": "No episodes for this season",
|
"no_episodes_for_this_season": "No episodes for this season",
|
||||||
"overview": "Overview",
|
"overview": "Overview",
|
||||||
"more_with": "More with {{name}}",
|
"more_with": "More with {{name}}",
|
||||||
@@ -627,10 +753,21 @@
|
|||||||
"media_options": "Media Options",
|
"media_options": "Media Options",
|
||||||
"quality": "Quality",
|
"quality": "Quality",
|
||||||
"audio": "Audio",
|
"audio": "Audio",
|
||||||
"subtitles": "Subtitle",
|
"subtitles": {
|
||||||
|
"label": "Subtitle",
|
||||||
|
"none": "None",
|
||||||
|
"tracks": "Tracks"
|
||||||
|
},
|
||||||
"show_more": "Show More",
|
"show_more": "Show More",
|
||||||
"show_less": "Show Less",
|
"show_less": "Show Less",
|
||||||
|
"left": "left",
|
||||||
|
"more_info": "More Info",
|
||||||
|
"director": "Director",
|
||||||
|
"cast": "Cast",
|
||||||
|
"technical_details": "Technical Details",
|
||||||
"appeared_in": "Appeared In",
|
"appeared_in": "Appeared In",
|
||||||
|
"movies": "Movies",
|
||||||
|
"shows": "Shows",
|
||||||
"could_not_load_item": "Could Not Load Item",
|
"could_not_load_item": "Could Not Load Item",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
"download": {
|
"download": {
|
||||||
@@ -641,7 +778,13 @@
|
|||||||
"download_x_item": "Download {{item_count}} Items",
|
"download_x_item": "Download {{item_count}} Items",
|
||||||
"download_unwatched_only": "Unwatched Only",
|
"download_unwatched_only": "Unwatched Only",
|
||||||
"download_button": "Download"
|
"download_button": "Download"
|
||||||
}
|
},
|
||||||
|
"mark_played": "Mark as Watched",
|
||||||
|
"mark_unplayed": "Mark as Unwatched",
|
||||||
|
"resume_playback": "Resume Playback",
|
||||||
|
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
|
||||||
|
"play_from_start": "Play from Start",
|
||||||
|
"continue_from": "Continue from {{time}}"
|
||||||
},
|
},
|
||||||
"live_tv": {
|
"live_tv": {
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
@@ -652,7 +795,18 @@
|
|||||||
"movies": "Movies",
|
"movies": "Movies",
|
||||||
"sports": "Sports",
|
"sports": "Sports",
|
||||||
"for_kids": "For Kids",
|
"for_kids": "For Kids",
|
||||||
"news": "News"
|
"news": "News",
|
||||||
|
"page_of": "Page {{current}} of {{total}}",
|
||||||
|
"no_programs": "No programs available",
|
||||||
|
"no_channels": "No channels available",
|
||||||
|
"tabs": {
|
||||||
|
"programs": "Programs",
|
||||||
|
"guide": "Guide",
|
||||||
|
"channels": "Channels",
|
||||||
|
"recordings": "Recordings",
|
||||||
|
"schedule": "Schedule",
|
||||||
|
"series": "Series"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"jellyseerr": {
|
"jellyseerr": {
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
@@ -697,6 +851,12 @@
|
|||||||
"decline": "Decline",
|
"decline": "Decline",
|
||||||
"requested_by": "Requested by {{user}}",
|
"requested_by": "Requested by {{user}}",
|
||||||
"unknown_user": "Unknown User",
|
"unknown_user": "Unknown User",
|
||||||
|
"select": "Select",
|
||||||
|
"request_all": "Request All",
|
||||||
|
"request_seasons": "Request Seasons",
|
||||||
|
"select_seasons": "Select Seasons",
|
||||||
|
"request_selected": "Request Selected",
|
||||||
|
"n_selected": "{{count}} selected",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"jellyseer_does_not_meet_requirements": "Seerr server does not meet minimum version requirements! Please update to at least 2.0.0",
|
"jellyseer_does_not_meet_requirements": "Seerr server does not meet minimum version requirements! Please update to at least 2.0.0",
|
||||||
"jellyseerr_test_failed": "Seerr test failed. Please try again.",
|
"jellyseerr_test_failed": "Seerr test failed. Please try again.",
|
||||||
@@ -716,7 +876,8 @@
|
|||||||
"search": "Search",
|
"search": "Search",
|
||||||
"library": "Library",
|
"library": "Library",
|
||||||
"custom_links": "Custom Links",
|
"custom_links": "Custom Links",
|
||||||
"favorites": "Favorites"
|
"favorites": "Favorites",
|
||||||
|
"settings": "Settings"
|
||||||
},
|
},
|
||||||
"music": {
|
"music": {
|
||||||
"title": "Music",
|
"title": "Music",
|
||||||
@@ -841,5 +1002,36 @@
|
|||||||
"show": "This show",
|
"show": "This show",
|
||||||
"all": "All media (default)"
|
"all": "All media (default)"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"companion_login": {
|
||||||
|
"title": "Pair with TV",
|
||||||
|
"align_qr": "Align the QR code within the frame",
|
||||||
|
"enter_code_manually": "Enter code manually",
|
||||||
|
"pairing_enter_credentials": "Enter credentials for TV",
|
||||||
|
"pairing_code_label": "Pairing code",
|
||||||
|
"server": "Server",
|
||||||
|
"authorize_button": "Authorize",
|
||||||
|
"authorizing": "Authorizing...",
|
||||||
|
"scan_again": "Scan Again",
|
||||||
|
"done": "Done",
|
||||||
|
"success_title": "Authorization Sent",
|
||||||
|
"pairing_tv_connecting": "The TV is connecting to your account",
|
||||||
|
"error_title": "Authorization Failed",
|
||||||
|
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
|
||||||
|
"error_generic": "Something went wrong. Please try again.",
|
||||||
|
"error_permission_denied": "Camera permission is required to scan QR codes.",
|
||||||
|
"login_as": "Log in as {{username}}?",
|
||||||
|
"on_server": "on {{server}}",
|
||||||
|
"use_different_user": "Use a different user",
|
||||||
|
"open_settings": "Open Settings"
|
||||||
|
},
|
||||||
|
"pairing": {
|
||||||
|
"pair_with_phone": "Pair with Phone",
|
||||||
|
"pair_with_phone_title": "Login TV",
|
||||||
|
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
|
||||||
|
"waiting_for_phone": "Waiting for phone...",
|
||||||
|
"scan_with_phone": "Scan with the Streamyfin app on your phone",
|
||||||
|
"logging_in": "Logging in...",
|
||||||
|
"logging_in_description": "Connecting to your server"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
"error_title": "Fout",
|
"error_title": "Fout",
|
||||||
"login_title": "Aanmelden",
|
"login_title": "Aanmelden",
|
||||||
"login_to_title": "Aanmelden bij",
|
"login_to_title": "Aanmelden bij",
|
||||||
|
"select_user": "Select a user to log in",
|
||||||
|
"add_user_to_login": "Add a user to log in",
|
||||||
|
"add_user": "Add User",
|
||||||
"username_placeholder": "Gebruikersnaam",
|
"username_placeholder": "Gebruikersnaam",
|
||||||
"password_placeholder": "Wachtwoord",
|
"password_placeholder": "Wachtwoord",
|
||||||
"login_button": "Aanmelden",
|
"login_button": "Aanmelden",
|
||||||
@@ -42,7 +45,13 @@
|
|||||||
"accounts_count": "{{count}} accounts",
|
"accounts_count": "{{count}} accounts",
|
||||||
"select_account": "Account selecteren",
|
"select_account": "Account selecteren",
|
||||||
"add_account": "Account toevoegen",
|
"add_account": "Account toevoegen",
|
||||||
"remove_account_description": "Hiermee worden de opgeslagen inloggegevens voor {{username}} verwijderd."
|
"remove_account_description": "Hiermee worden de opgeslagen inloggegevens voor {{username}} verwijderd.",
|
||||||
|
"remove_server": "Remove Server",
|
||||||
|
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
|
||||||
|
"select_your_server": "Select Your Server",
|
||||||
|
"add_server_to_get_started": "Add a server to get started",
|
||||||
|
"add_server": "Add Server",
|
||||||
|
"change_server": "Change Server"
|
||||||
},
|
},
|
||||||
"save_account": {
|
"save_account": {
|
||||||
"title": "Account opslaan",
|
"title": "Account opslaan",
|
||||||
@@ -86,6 +95,7 @@
|
|||||||
"oops": "Oeps!",
|
"oops": "Oeps!",
|
||||||
"error_message": "Er ging iets fout\nProbeer opnieuw in te loggen.",
|
"error_message": "Er ging iets fout\nProbeer opnieuw in te loggen.",
|
||||||
"continue_watching": "Verder Kijken",
|
"continue_watching": "Verder Kijken",
|
||||||
|
"continue": "Continue",
|
||||||
"next_up": "Volgende",
|
"next_up": "Volgende",
|
||||||
"continue_and_next_up": "Doorgaan & Volgende",
|
"continue_and_next_up": "Doorgaan & Volgende",
|
||||||
"recently_added_in": "Recent toegevoegd in {{libraryName}}",
|
"recently_added_in": "Recent toegevoegd in {{libraryName}}",
|
||||||
@@ -109,6 +119,12 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"settings_title": "Instellingen",
|
"settings_title": "Instellingen",
|
||||||
"log_out_button": "Afmelden",
|
"log_out_button": "Afmelden",
|
||||||
|
"switch_user": {
|
||||||
|
"title": "Switch User",
|
||||||
|
"account": "Account",
|
||||||
|
"switch_user": "Switch User on This Server",
|
||||||
|
"current": "current"
|
||||||
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "Categorieën"
|
"title": "Categorieën"
|
||||||
},
|
},
|
||||||
@@ -121,7 +137,16 @@
|
|||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "Weergave",
|
"title": "Weergave",
|
||||||
"merge_next_up_continue_watching": "Doorgaan met kijken & Volgende samenvoegen",
|
"merge_next_up_continue_watching": "Doorgaan met kijken & Volgende samenvoegen",
|
||||||
"hide_remote_session_button": "Verberg Knop voor Externe Sessie"
|
"hide_remote_session_button": "Verberg Knop voor Externe Sessie",
|
||||||
|
"show_home_backdrop": "Dynamic Home Backdrop",
|
||||||
|
"show_hero_carousel": "Hero Carousel",
|
||||||
|
"show_series_poster_on_episode": "Show Series Poster on Episodes",
|
||||||
|
"theme_music": "Theme Music",
|
||||||
|
"display_size": "Display Size",
|
||||||
|
"display_size_small": "Small",
|
||||||
|
"display_size_default": "Default",
|
||||||
|
"display_size_large": "Large",
|
||||||
|
"display_size_extra_large": "Extra Large"
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"title": "Netwerk",
|
"title": "Netwerk",
|
||||||
@@ -174,6 +199,22 @@
|
|||||||
"rewind_length": "Duur terugspoelen",
|
"rewind_length": "Duur terugspoelen",
|
||||||
"seconds_unit": "s"
|
"seconds_unit": "s"
|
||||||
},
|
},
|
||||||
|
"buffer": {
|
||||||
|
"title": "Buffer Settings",
|
||||||
|
"cache_mode": "Cache Mode",
|
||||||
|
"cache_auto": "Auto",
|
||||||
|
"cache_yes": "Enabled",
|
||||||
|
"cache_no": "Disabled",
|
||||||
|
"buffer_duration": "Buffer Duration",
|
||||||
|
"max_cache_size": "Max Cache Size",
|
||||||
|
"max_backward_cache": "Max Backward Cache"
|
||||||
|
},
|
||||||
|
"vo_driver": {
|
||||||
|
"title": "Video Output",
|
||||||
|
"vo_mode": "VO Driver",
|
||||||
|
"gpu_next": "gpu-next (Recommended)",
|
||||||
|
"gpu": "gpu"
|
||||||
|
},
|
||||||
"gesture_controls": {
|
"gesture_controls": {
|
||||||
"gesture_controls_title": "Gebaar Bediening",
|
"gesture_controls_title": "Gebaar Bediening",
|
||||||
"horizontal_swipe_skip": "Horizontale Swipe om over te slaan",
|
"horizontal_swipe_skip": "Horizontale Swipe om over te slaan",
|
||||||
@@ -256,7 +297,23 @@
|
|||||||
"subtitle_font": "Lettertype ondertitels",
|
"subtitle_font": "Lettertype ondertitels",
|
||||||
"ksplayer_title": "KSPlayer Instellingen",
|
"ksplayer_title": "KSPlayer Instellingen",
|
||||||
"hardware_decode": "Hardware Acceleratie",
|
"hardware_decode": "Hardware Acceleratie",
|
||||||
"hardware_decode_description": "Gebruik hardware acceleratie voor video-decodering. Uitschakelen als u problemen met afspelen ondervindt."
|
"hardware_decode_description": "Gebruik hardware acceleratie voor video-decodering. Uitschakelen als u problemen met afspelen ondervindt.",
|
||||||
|
"opensubtitles_title": "OpenSubtitles",
|
||||||
|
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
|
||||||
|
"opensubtitles_api_key": "API Key",
|
||||||
|
"opensubtitles_api_key_placeholder": "Enter API key...",
|
||||||
|
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
|
||||||
|
"mpv_subtitle_scale": "Subtitle Scale",
|
||||||
|
"mpv_subtitle_margin_y": "Vertical Margin",
|
||||||
|
"mpv_subtitle_align_x": "Horizontal Align",
|
||||||
|
"mpv_subtitle_align_y": "Vertical Align",
|
||||||
|
"align": {
|
||||||
|
"left": "Left",
|
||||||
|
"center": "Center",
|
||||||
|
"right": "Right",
|
||||||
|
"top": "Top",
|
||||||
|
"bottom": "Bottom"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"vlc_subtitles": {
|
"vlc_subtitles": {
|
||||||
"title": "VLC ondertitel instellingen",
|
"title": "VLC ondertitel instellingen",
|
||||||
@@ -406,7 +463,13 @@
|
|||||||
"music_cache_cleared": "Muziek cache gewist",
|
"music_cache_cleared": "Muziek cache gewist",
|
||||||
"delete_all_downloaded_songs": "Verwijder alle gedownloade liedjes",
|
"delete_all_downloaded_songs": "Verwijder alle gedownloade liedjes",
|
||||||
"downloaded_songs_size": "{{size}} gedownload",
|
"downloaded_songs_size": "{{size}} gedownload",
|
||||||
"downloaded_songs_deleted": "Downloaded songs deleted"
|
"downloaded_songs_deleted": "Downloaded songs deleted",
|
||||||
|
"clear_all_cache": "Clear All Cache",
|
||||||
|
"clear_all_cache_confirm": "Clear All Cache?",
|
||||||
|
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
|
||||||
|
"clear_all_cache_success": "Cache Cleared",
|
||||||
|
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
|
||||||
|
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "Intro",
|
"title": "Intro",
|
||||||
@@ -430,6 +493,21 @@
|
|||||||
"error_deleting_files": "Fout bij het verwijderen van bestanden",
|
"error_deleting_files": "Fout bij het verwijderen van bestanden",
|
||||||
"background_downloads_enabled": "Downloads op de achtergrond ingeschakeld",
|
"background_downloads_enabled": "Downloads op de achtergrond ingeschakeld",
|
||||||
"background_downloads_disabled": "Downloads op de achtergrond uitgeschakeld"
|
"background_downloads_disabled": "Downloads op de achtergrond uitgeschakeld"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"title": "Security",
|
||||||
|
"inactivity_timeout": {
|
||||||
|
"title": "Inactivity Timeout",
|
||||||
|
"description": "Auto logout after inactivity",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"1_minute": "1 minute",
|
||||||
|
"5_minutes": "5 minutes",
|
||||||
|
"15_minutes": "15 minutes",
|
||||||
|
"30_minutes": "30 minutes",
|
||||||
|
"1_hour": "1 hour",
|
||||||
|
"4_hours": "4 hours",
|
||||||
|
"24_hours": "24 hours"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -456,6 +534,7 @@
|
|||||||
"new_app_version_requires_re_download_description": "Voor de nieuwe update moet de content opnieuw worden gedownload. Verwijder alle gedownloade content en probeer het opnieuw.",
|
"new_app_version_requires_re_download_description": "Voor de nieuwe update moet de content opnieuw worden gedownload. Verwijder alle gedownloade content en probeer het opnieuw.",
|
||||||
"back": "Terug",
|
"back": "Terug",
|
||||||
"delete": "Verwijder",
|
"delete": "Verwijder",
|
||||||
|
"delete_download": "Delete Download",
|
||||||
"something_went_wrong": "Er ging iets mis",
|
"something_went_wrong": "Er ging iets mis",
|
||||||
"could_not_get_stream_url_from_jellyfin": "Kon de URL van de stream niet krijgen van Jellyfin",
|
"could_not_get_stream_url_from_jellyfin": "Kon de URL van de stream niet krijgen van Jellyfin",
|
||||||
"eta": "ETA {{eta}}",
|
"eta": "ETA {{eta}}",
|
||||||
@@ -492,22 +571,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"no_results": "No Results",
|
||||||
"select": "Selecteren",
|
"select": "Selecteren",
|
||||||
"no_trailer_available": "Geen trailer beschikbaar",
|
"no_trailer_available": "Geen trailer beschikbaar",
|
||||||
"video": "Video",
|
"video": "Video",
|
||||||
"audio": "Audio",
|
"audio": "Audio",
|
||||||
"subtitle": "Ondertitel",
|
"subtitle": "Ondertitel",
|
||||||
"play": "Afspelen",
|
"play": "Afspelen",
|
||||||
|
"mark_as_played": "Mark as Played",
|
||||||
|
"mark_as_not_played": "Mark as not Played",
|
||||||
"none": "Geen",
|
"none": "Geen",
|
||||||
"track": "Spoor",
|
"track": "Spoor",
|
||||||
"cancel": "Annuleren",
|
"cancel": "Annuleren",
|
||||||
|
"stop": "Stop",
|
||||||
"delete": "Verwijderen",
|
"delete": "Verwijderen",
|
||||||
"ok": "Oké",
|
"ok": "Oké",
|
||||||
"remove": "Verwijderen",
|
"remove": "Verwijderen",
|
||||||
"next": "Volgende",
|
"next": "Volgende",
|
||||||
"back": "Terug",
|
"back": "Terug",
|
||||||
"continue": "Doorgaan",
|
"continue": "Doorgaan",
|
||||||
"verifying": "Verifiëren..."
|
"verifying": "Verifiëren...",
|
||||||
|
"login": "Login",
|
||||||
|
"refresh": "Refresh"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"search": "Zoek...",
|
"search": "Zoek...",
|
||||||
@@ -556,6 +641,7 @@
|
|||||||
"movies": "Films",
|
"movies": "Films",
|
||||||
"series": "Series",
|
"series": "Series",
|
||||||
"boxsets": "Boxsets",
|
"boxsets": "Boxsets",
|
||||||
|
"playlists": "Playlists",
|
||||||
"items": "items"
|
"items": "items"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
@@ -566,7 +652,8 @@
|
|||||||
"poster": "Poster",
|
"poster": "Poster",
|
||||||
"cover": "Omslag",
|
"cover": "Omslag",
|
||||||
"show_titles": "Toon titels",
|
"show_titles": "Toon titels",
|
||||||
"show_stats": "Toon statistieken"
|
"show_stats": "Toon statistieken",
|
||||||
|
"options_title": "Options"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"genres": "Genres",
|
"genres": "Genres",
|
||||||
@@ -574,7 +661,11 @@
|
|||||||
"sort_by": "Sorteren op",
|
"sort_by": "Sorteren op",
|
||||||
"filter_by": "Filteren op",
|
"filter_by": "Filteren op",
|
||||||
"sort_order": "Sorteer volgorde",
|
"sort_order": "Sorteer volgorde",
|
||||||
"tags": "Labels"
|
"tags": "Labels",
|
||||||
|
"all": "All",
|
||||||
|
"reset": "Reset",
|
||||||
|
"asc": "Ascending",
|
||||||
|
"desc": "Descending"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"favorites": {
|
"favorites": {
|
||||||
@@ -591,6 +682,8 @@
|
|||||||
"no_links": "Geen links"
|
"no_links": "Geen links"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
|
"live": "LIVE",
|
||||||
|
"mpv_player_title": "MPV Player",
|
||||||
"error": "Fout",
|
"error": "Fout",
|
||||||
"failed_to_get_stream_url": "De stream-URL kon niet worden verkregen",
|
"failed_to_get_stream_url": "De stream-URL kon niet worden verkregen",
|
||||||
"an_error_occured_while_playing_the_video": "Er is een fout opgetreden tijdens het afspelen van de video. Controleer de logs in de instellingen.",
|
"an_error_occured_while_playing_the_video": "Er is een fout opgetreden tijdens het afspelen van de video. Controleer de logs in de instellingen.",
|
||||||
@@ -608,7 +701,35 @@
|
|||||||
"downloaded_file_message": "Wil je het gedownloade bestand afspelen?",
|
"downloaded_file_message": "Wil je het gedownloade bestand afspelen?",
|
||||||
"downloaded_file_yes": "Ja",
|
"downloaded_file_yes": "Ja",
|
||||||
"downloaded_file_no": "Nee",
|
"downloaded_file_no": "Nee",
|
||||||
"downloaded_file_cancel": "Annuleren"
|
"downloaded_file_cancel": "Annuleren",
|
||||||
|
"swipe_down_settings": "Swipe down for settings",
|
||||||
|
"ends_at": "Ends at {{time}}",
|
||||||
|
"search_subtitles": "Search Subtitles",
|
||||||
|
"subtitle_tracks": "Tracks",
|
||||||
|
"subtitle_search": "Search & Download",
|
||||||
|
"download": "Download",
|
||||||
|
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
|
||||||
|
"using_jellyfin_server": "Using Jellyfin Server",
|
||||||
|
"language": "Language",
|
||||||
|
"results": "Results",
|
||||||
|
"searching": "Searching...",
|
||||||
|
"search_failed": "Search failed",
|
||||||
|
"no_subtitle_provider": "No subtitle provider configured on server",
|
||||||
|
"no_subtitles_found": "No subtitles found",
|
||||||
|
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
|
||||||
|
"settings": "Settings",
|
||||||
|
"skip_intro": "Skip Intro",
|
||||||
|
"skip_credits": "Skip Credits",
|
||||||
|
"stopPlayback": "Stop Playback",
|
||||||
|
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
|
||||||
|
"stopPlayingConfirm": "Are you sure you want to stop playback?",
|
||||||
|
"downloaded": "Downloaded"
|
||||||
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Volgende",
|
"next_up": "Volgende",
|
||||||
@@ -617,6 +738,11 @@
|
|||||||
"series": "Series",
|
"series": "Series",
|
||||||
"seasons": "Seizoenen",
|
"seasons": "Seizoenen",
|
||||||
"season": "Seizoen",
|
"season": "Seizoen",
|
||||||
|
"from_this_series": "From This Series",
|
||||||
|
"more_from_this_season": "More from this Season",
|
||||||
|
"view_series": "View Series",
|
||||||
|
"view_season": "View Season",
|
||||||
|
"select_season": "Select Season",
|
||||||
"no_episodes_for_this_season": "Geen afleveringen voor dit seizoen",
|
"no_episodes_for_this_season": "Geen afleveringen voor dit seizoen",
|
||||||
"overview": "Overzicht",
|
"overview": "Overzicht",
|
||||||
"more_with": "Meer met {{name}}",
|
"more_with": "Meer met {{name}}",
|
||||||
@@ -627,10 +753,21 @@
|
|||||||
"media_options": "Media opties",
|
"media_options": "Media opties",
|
||||||
"quality": "Kwaliteit",
|
"quality": "Kwaliteit",
|
||||||
"audio": "Audio",
|
"audio": "Audio",
|
||||||
"subtitles": "Ondertitel",
|
"subtitles": {
|
||||||
|
"label": "Subtitle",
|
||||||
|
"none": "None",
|
||||||
|
"tracks": "Tracks"
|
||||||
|
},
|
||||||
"show_more": "Toon meer",
|
"show_more": "Toon meer",
|
||||||
"show_less": "Toon minder",
|
"show_less": "Toon minder",
|
||||||
|
"left": "left",
|
||||||
|
"more_info": "More Info",
|
||||||
|
"director": "Director",
|
||||||
|
"cast": "Cast",
|
||||||
|
"technical_details": "Technical Details",
|
||||||
"appeared_in": "Verschenen in",
|
"appeared_in": "Verschenen in",
|
||||||
|
"movies": "Movies",
|
||||||
|
"shows": "Shows",
|
||||||
"could_not_load_item": "Kon item niet laden",
|
"could_not_load_item": "Kon item niet laden",
|
||||||
"none": "Geen",
|
"none": "Geen",
|
||||||
"download": {
|
"download": {
|
||||||
@@ -641,7 +778,13 @@
|
|||||||
"download_x_item": "Download {{item_count}} items",
|
"download_x_item": "Download {{item_count}} items",
|
||||||
"download_unwatched_only": "Alleen niet bekeken",
|
"download_unwatched_only": "Alleen niet bekeken",
|
||||||
"download_button": "Downloaden"
|
"download_button": "Downloaden"
|
||||||
}
|
},
|
||||||
|
"mark_played": "Mark as Watched",
|
||||||
|
"mark_unplayed": "Mark as Unwatched",
|
||||||
|
"resume_playback": "Resume Playback",
|
||||||
|
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
|
||||||
|
"play_from_start": "Play from Start",
|
||||||
|
"continue_from": "Continue from {{time}}"
|
||||||
},
|
},
|
||||||
"live_tv": {
|
"live_tv": {
|
||||||
"next": "Volgende ",
|
"next": "Volgende ",
|
||||||
@@ -652,7 +795,18 @@
|
|||||||
"movies": "Films",
|
"movies": "Films",
|
||||||
"sports": "Sport",
|
"sports": "Sport",
|
||||||
"for_kids": "Voor kinderen",
|
"for_kids": "Voor kinderen",
|
||||||
"news": "Nieuws"
|
"news": "Nieuws",
|
||||||
|
"page_of": "Page {{current}} of {{total}}",
|
||||||
|
"no_programs": "No programs available",
|
||||||
|
"no_channels": "No channels available",
|
||||||
|
"tabs": {
|
||||||
|
"programs": "Programs",
|
||||||
|
"guide": "Guide",
|
||||||
|
"channels": "Channels",
|
||||||
|
"recordings": "Recordings",
|
||||||
|
"schedule": "Schedule",
|
||||||
|
"series": "Series"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"jellyseerr": {
|
"jellyseerr": {
|
||||||
"confirm": "Bevestig",
|
"confirm": "Bevestig",
|
||||||
@@ -697,6 +851,12 @@
|
|||||||
"decline": "Weigeren",
|
"decline": "Weigeren",
|
||||||
"requested_by": "Aangevraagd door {{user}}",
|
"requested_by": "Aangevraagd door {{user}}",
|
||||||
"unknown_user": "Onbekende gebruiker",
|
"unknown_user": "Onbekende gebruiker",
|
||||||
|
"select": "Select",
|
||||||
|
"request_all": "Request All",
|
||||||
|
"request_seasons": "Request Seasons",
|
||||||
|
"select_seasons": "Select Seasons",
|
||||||
|
"request_selected": "Request Selected",
|
||||||
|
"n_selected": "{{count}} selected",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"jellyseer_does_not_meet_requirements": "Jellyseerr server voldoet niet aan de minimale versievereisten! Update naar minimaal 2.0.0",
|
"jellyseer_does_not_meet_requirements": "Jellyseerr server voldoet niet aan de minimale versievereisten! Update naar minimaal 2.0.0",
|
||||||
"jellyseerr_test_failed": "Jellyseerr test mislukt. Probeer opnieuw.",
|
"jellyseerr_test_failed": "Jellyseerr test mislukt. Probeer opnieuw.",
|
||||||
@@ -716,7 +876,8 @@
|
|||||||
"search": "Zoeken",
|
"search": "Zoeken",
|
||||||
"library": "Bibliotheek",
|
"library": "Bibliotheek",
|
||||||
"custom_links": "Aangepaste links",
|
"custom_links": "Aangepaste links",
|
||||||
"favorites": "Favorieten"
|
"favorites": "Favorieten",
|
||||||
|
"settings": "Settings"
|
||||||
},
|
},
|
||||||
"music": {
|
"music": {
|
||||||
"title": "Muziek",
|
"title": "Muziek",
|
||||||
@@ -841,5 +1002,36 @@
|
|||||||
"show": "Deze serie",
|
"show": "Deze serie",
|
||||||
"all": "Alle media (standaard)"
|
"all": "Alle media (standaard)"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"companion_login": {
|
||||||
|
"title": "Pair with TV",
|
||||||
|
"align_qr": "Align the QR code within the frame",
|
||||||
|
"enter_code_manually": "Enter code manually",
|
||||||
|
"pairing_enter_credentials": "Enter credentials for TV",
|
||||||
|
"pairing_code_label": "Pairing code",
|
||||||
|
"server": "Server",
|
||||||
|
"authorize_button": "Authorize",
|
||||||
|
"authorizing": "Authorizing...",
|
||||||
|
"scan_again": "Scan Again",
|
||||||
|
"done": "Done",
|
||||||
|
"success_title": "Authorization Sent",
|
||||||
|
"pairing_tv_connecting": "The TV is connecting to your account",
|
||||||
|
"error_title": "Authorization Failed",
|
||||||
|
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
|
||||||
|
"error_generic": "Something went wrong. Please try again.",
|
||||||
|
"error_permission_denied": "Camera permission is required to scan QR codes.",
|
||||||
|
"login_as": "Log in as {{username}}?",
|
||||||
|
"on_server": "on {{server}}",
|
||||||
|
"use_different_user": "Use a different user",
|
||||||
|
"open_settings": "Open Settings"
|
||||||
|
},
|
||||||
|
"pairing": {
|
||||||
|
"pair_with_phone": "Pair with Phone",
|
||||||
|
"pair_with_phone_title": "Login TV",
|
||||||
|
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
|
||||||
|
"waiting_for_phone": "Waiting for phone...",
|
||||||
|
"scan_with_phone": "Scan with the Streamyfin app on your phone",
|
||||||
|
"logging_in": "Logging in...",
|
||||||
|
"logging_in_description": "Connecting to your server"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
"error_title": "Feil",
|
"error_title": "Feil",
|
||||||
"login_title": "Logg inn",
|
"login_title": "Logg inn",
|
||||||
"login_to_title": "Logg inn i",
|
"login_to_title": "Logg inn i",
|
||||||
|
"select_user": "Select a user to log in",
|
||||||
|
"add_user_to_login": "Add a user to log in",
|
||||||
|
"add_user": "Add User",
|
||||||
"username_placeholder": "Brukernavn",
|
"username_placeholder": "Brukernavn",
|
||||||
"password_placeholder": "Passord",
|
"password_placeholder": "Passord",
|
||||||
"login_button": "Logg inn",
|
"login_button": "Logg inn",
|
||||||
@@ -42,7 +45,13 @@
|
|||||||
"accounts_count": "{{count}} accounts",
|
"accounts_count": "{{count}} accounts",
|
||||||
"select_account": "Select Account",
|
"select_account": "Select Account",
|
||||||
"add_account": "Add Account",
|
"add_account": "Add Account",
|
||||||
"remove_account_description": "This will remove the saved credentials for {{username}}."
|
"remove_account_description": "This will remove the saved credentials for {{username}}.",
|
||||||
|
"remove_server": "Remove Server",
|
||||||
|
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
|
||||||
|
"select_your_server": "Select Your Server",
|
||||||
|
"add_server_to_get_started": "Add a server to get started",
|
||||||
|
"add_server": "Add Server",
|
||||||
|
"change_server": "Change Server"
|
||||||
},
|
},
|
||||||
"save_account": {
|
"save_account": {
|
||||||
"title": "Save Account",
|
"title": "Save Account",
|
||||||
@@ -86,6 +95,7 @@
|
|||||||
"oops": "Oisann!",
|
"oops": "Oisann!",
|
||||||
"error_message": "Noe gikk galt.\nVennligst logg ut og inn igjen.",
|
"error_message": "Noe gikk galt.\nVennligst logg ut og inn igjen.",
|
||||||
"continue_watching": "Fortsett å se",
|
"continue_watching": "Fortsett å se",
|
||||||
|
"continue": "Continue",
|
||||||
"next_up": "Neste opp",
|
"next_up": "Neste opp",
|
||||||
"continue_and_next_up": "Continue & Next Up",
|
"continue_and_next_up": "Continue & Next Up",
|
||||||
"recently_added_in": "Nylig lagt til i {{libraryName}}",
|
"recently_added_in": "Nylig lagt til i {{libraryName}}",
|
||||||
@@ -109,6 +119,12 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"settings_title": "Innstillinger",
|
"settings_title": "Innstillinger",
|
||||||
"log_out_button": "Logg ut",
|
"log_out_button": "Logg ut",
|
||||||
|
"switch_user": {
|
||||||
|
"title": "Switch User",
|
||||||
|
"account": "Account",
|
||||||
|
"switch_user": "Switch User on This Server",
|
||||||
|
"current": "current"
|
||||||
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "Categories"
|
"title": "Categories"
|
||||||
},
|
},
|
||||||
@@ -121,7 +137,16 @@
|
|||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "Appearance",
|
"title": "Appearance",
|
||||||
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
||||||
"hide_remote_session_button": "Hide Remote Session Button"
|
"hide_remote_session_button": "Hide Remote Session Button",
|
||||||
|
"show_home_backdrop": "Dynamic Home Backdrop",
|
||||||
|
"show_hero_carousel": "Hero Carousel",
|
||||||
|
"show_series_poster_on_episode": "Show Series Poster on Episodes",
|
||||||
|
"theme_music": "Theme Music",
|
||||||
|
"display_size": "Display Size",
|
||||||
|
"display_size_small": "Small",
|
||||||
|
"display_size_default": "Default",
|
||||||
|
"display_size_large": "Large",
|
||||||
|
"display_size_extra_large": "Extra Large"
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"title": "Network",
|
"title": "Network",
|
||||||
@@ -174,6 +199,22 @@
|
|||||||
"rewind_length": "Omspar lengde",
|
"rewind_length": "Omspar lengde",
|
||||||
"seconds_unit": "S"
|
"seconds_unit": "S"
|
||||||
},
|
},
|
||||||
|
"buffer": {
|
||||||
|
"title": "Buffer Settings",
|
||||||
|
"cache_mode": "Cache Mode",
|
||||||
|
"cache_auto": "Auto",
|
||||||
|
"cache_yes": "Enabled",
|
||||||
|
"cache_no": "Disabled",
|
||||||
|
"buffer_duration": "Buffer Duration",
|
||||||
|
"max_cache_size": "Max Cache Size",
|
||||||
|
"max_backward_cache": "Max Backward Cache"
|
||||||
|
},
|
||||||
|
"vo_driver": {
|
||||||
|
"title": "Video Output",
|
||||||
|
"vo_mode": "VO Driver",
|
||||||
|
"gpu_next": "gpu-next (Recommended)",
|
||||||
|
"gpu": "gpu"
|
||||||
|
},
|
||||||
"gesture_controls": {
|
"gesture_controls": {
|
||||||
"gesture_controls_title": "Gest kontroller",
|
"gesture_controls_title": "Gest kontroller",
|
||||||
"horizontal_swipe_skip": "Vannrett sveip for å hoppe over",
|
"horizontal_swipe_skip": "Vannrett sveip for å hoppe over",
|
||||||
@@ -256,7 +297,23 @@
|
|||||||
"subtitle_font": "Subtitle Font",
|
"subtitle_font": "Subtitle Font",
|
||||||
"ksplayer_title": "KSPlayer Settings",
|
"ksplayer_title": "KSPlayer Settings",
|
||||||
"hardware_decode": "Hardware Decoding",
|
"hardware_decode": "Hardware Decoding",
|
||||||
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
|
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.",
|
||||||
|
"opensubtitles_title": "OpenSubtitles",
|
||||||
|
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
|
||||||
|
"opensubtitles_api_key": "API Key",
|
||||||
|
"opensubtitles_api_key_placeholder": "Enter API key...",
|
||||||
|
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
|
||||||
|
"mpv_subtitle_scale": "Subtitle Scale",
|
||||||
|
"mpv_subtitle_margin_y": "Vertical Margin",
|
||||||
|
"mpv_subtitle_align_x": "Horizontal Align",
|
||||||
|
"mpv_subtitle_align_y": "Vertical Align",
|
||||||
|
"align": {
|
||||||
|
"left": "Left",
|
||||||
|
"center": "Center",
|
||||||
|
"right": "Right",
|
||||||
|
"top": "Top",
|
||||||
|
"bottom": "Bottom"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"vlc_subtitles": {
|
"vlc_subtitles": {
|
||||||
"title": "VLC Subtitle Settings",
|
"title": "VLC Subtitle Settings",
|
||||||
@@ -406,7 +463,13 @@
|
|||||||
"music_cache_cleared": "Music cache cleared",
|
"music_cache_cleared": "Music cache cleared",
|
||||||
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
||||||
"downloaded_songs_size": "{{size}} downloaded",
|
"downloaded_songs_size": "{{size}} downloaded",
|
||||||
"downloaded_songs_deleted": "Downloaded songs deleted"
|
"downloaded_songs_deleted": "Downloaded songs deleted",
|
||||||
|
"clear_all_cache": "Clear All Cache",
|
||||||
|
"clear_all_cache_confirm": "Clear All Cache?",
|
||||||
|
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
|
||||||
|
"clear_all_cache_success": "Cache Cleared",
|
||||||
|
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
|
||||||
|
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "Intro",
|
"title": "Intro",
|
||||||
@@ -430,6 +493,21 @@
|
|||||||
"error_deleting_files": "Feil ved sletting av filer",
|
"error_deleting_files": "Feil ved sletting av filer",
|
||||||
"background_downloads_enabled": "Nedlastinger av bakgrunn aktivert",
|
"background_downloads_enabled": "Nedlastinger av bakgrunn aktivert",
|
||||||
"background_downloads_disabled": "Bakgrunnsnedlastinger deaktivert"
|
"background_downloads_disabled": "Bakgrunnsnedlastinger deaktivert"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"title": "Security",
|
||||||
|
"inactivity_timeout": {
|
||||||
|
"title": "Inactivity Timeout",
|
||||||
|
"description": "Auto logout after inactivity",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"1_minute": "1 minute",
|
||||||
|
"5_minutes": "5 minutes",
|
||||||
|
"15_minutes": "15 minutes",
|
||||||
|
"30_minutes": "30 minutes",
|
||||||
|
"1_hour": "1 hour",
|
||||||
|
"4_hours": "4 hours",
|
||||||
|
"24_hours": "24 hours"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -456,6 +534,7 @@
|
|||||||
"new_app_version_requires_re_download_description": "Den nye oppdateringen krever at innholdet lastes ned på nytt. Fjern alt nedlastet innhold og prøv på nytt.",
|
"new_app_version_requires_re_download_description": "Den nye oppdateringen krever at innholdet lastes ned på nytt. Fjern alt nedlastet innhold og prøv på nytt.",
|
||||||
"back": "Tilbake",
|
"back": "Tilbake",
|
||||||
"delete": "Slett",
|
"delete": "Slett",
|
||||||
|
"delete_download": "Delete Download",
|
||||||
"something_went_wrong": "Noe gikk galt",
|
"something_went_wrong": "Noe gikk galt",
|
||||||
"could_not_get_stream_url_from_jellyfin": "Kunne ikke hente stream-URL fra Jellyfin",
|
"could_not_get_stream_url_from_jellyfin": "Kunne ikke hente stream-URL fra Jellyfin",
|
||||||
"eta": "ETA {{eta}}",
|
"eta": "ETA {{eta}}",
|
||||||
@@ -492,22 +571,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"no_results": "No Results",
|
||||||
"select": "Velg",
|
"select": "Velg",
|
||||||
"no_trailer_available": "Ingen trailer tilgjengelig",
|
"no_trailer_available": "Ingen trailer tilgjengelig",
|
||||||
"video": "Video",
|
"video": "Video",
|
||||||
"audio": "Lyd",
|
"audio": "Lyd",
|
||||||
"subtitle": "Undertittel",
|
"subtitle": "Undertittel",
|
||||||
"play": "Spill",
|
"play": "Spill",
|
||||||
|
"mark_as_played": "Mark as Played",
|
||||||
|
"mark_as_not_played": "Mark as not Played",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
"track": "Track",
|
"track": "Track",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
|
"stop": "Stop",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"verifying": "Verifying..."
|
"verifying": "Verifying...",
|
||||||
|
"login": "Login",
|
||||||
|
"refresh": "Refresh"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"search": "Søk...",
|
"search": "Søk...",
|
||||||
@@ -556,6 +641,7 @@
|
|||||||
"movies": "Filmer",
|
"movies": "Filmer",
|
||||||
"series": "Serier",
|
"series": "Serier",
|
||||||
"boxsets": "Boks sett",
|
"boxsets": "Boks sett",
|
||||||
|
"playlists": "Playlists",
|
||||||
"items": "Elementer"
|
"items": "Elementer"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
@@ -566,7 +652,8 @@
|
|||||||
"poster": "Plakat",
|
"poster": "Plakat",
|
||||||
"cover": "Omslag",
|
"cover": "Omslag",
|
||||||
"show_titles": "Vis titler",
|
"show_titles": "Vis titler",
|
||||||
"show_stats": "Vis statistikk"
|
"show_stats": "Vis statistikk",
|
||||||
|
"options_title": "Options"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"genres": "Genres",
|
"genres": "Genres",
|
||||||
@@ -574,7 +661,11 @@
|
|||||||
"sort_by": "Sorter etter",
|
"sort_by": "Sorter etter",
|
||||||
"filter_by": "Filter By",
|
"filter_by": "Filter By",
|
||||||
"sort_order": "Sorter etter",
|
"sort_order": "Sorter etter",
|
||||||
"tags": "Tagger"
|
"tags": "Tagger",
|
||||||
|
"all": "All",
|
||||||
|
"reset": "Reset",
|
||||||
|
"asc": "Ascending",
|
||||||
|
"desc": "Descending"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"favorites": {
|
"favorites": {
|
||||||
@@ -591,6 +682,8 @@
|
|||||||
"no_links": "Ingen lenke"
|
"no_links": "Ingen lenke"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
|
"live": "LIVE",
|
||||||
|
"mpv_player_title": "MPV Player",
|
||||||
"error": "Feil",
|
"error": "Feil",
|
||||||
"failed_to_get_stream_url": "Kan ikke hente nettadressen for stream",
|
"failed_to_get_stream_url": "Kan ikke hente nettadressen for stream",
|
||||||
"an_error_occured_while_playing_the_video": "En feil oppstod under video. Sjekk loggene i innstillingene.",
|
"an_error_occured_while_playing_the_video": "En feil oppstod under video. Sjekk loggene i innstillingene.",
|
||||||
@@ -608,7 +701,35 @@
|
|||||||
"downloaded_file_message": "Do you want to play the downloaded file?",
|
"downloaded_file_message": "Do you want to play the downloaded file?",
|
||||||
"downloaded_file_yes": "Yes",
|
"downloaded_file_yes": "Yes",
|
||||||
"downloaded_file_no": "No",
|
"downloaded_file_no": "No",
|
||||||
"downloaded_file_cancel": "Cancel"
|
"downloaded_file_cancel": "Cancel",
|
||||||
|
"swipe_down_settings": "Swipe down for settings",
|
||||||
|
"ends_at": "Ends at {{time}}",
|
||||||
|
"search_subtitles": "Search Subtitles",
|
||||||
|
"subtitle_tracks": "Tracks",
|
||||||
|
"subtitle_search": "Search & Download",
|
||||||
|
"download": "Download",
|
||||||
|
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
|
||||||
|
"using_jellyfin_server": "Using Jellyfin Server",
|
||||||
|
"language": "Language",
|
||||||
|
"results": "Results",
|
||||||
|
"searching": "Searching...",
|
||||||
|
"search_failed": "Search failed",
|
||||||
|
"no_subtitle_provider": "No subtitle provider configured on server",
|
||||||
|
"no_subtitles_found": "No subtitles found",
|
||||||
|
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
|
||||||
|
"settings": "Settings",
|
||||||
|
"skip_intro": "Skip Intro",
|
||||||
|
"skip_credits": "Skip Credits",
|
||||||
|
"stopPlayback": "Stop Playback",
|
||||||
|
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
|
||||||
|
"stopPlayingConfirm": "Are you sure you want to stop playback?",
|
||||||
|
"downloaded": "Downloaded"
|
||||||
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Neste opp",
|
"next_up": "Neste opp",
|
||||||
@@ -617,6 +738,11 @@
|
|||||||
"series": "Serier",
|
"series": "Serier",
|
||||||
"seasons": "Sesonger",
|
"seasons": "Sesonger",
|
||||||
"season": "Sesong",
|
"season": "Sesong",
|
||||||
|
"from_this_series": "From This Series",
|
||||||
|
"more_from_this_season": "More from this Season",
|
||||||
|
"view_series": "View Series",
|
||||||
|
"view_season": "View Season",
|
||||||
|
"select_season": "Select Season",
|
||||||
"no_episodes_for_this_season": "Ingen episoder for denne sesongen",
|
"no_episodes_for_this_season": "Ingen episoder for denne sesongen",
|
||||||
"overview": "Oversikt",
|
"overview": "Oversikt",
|
||||||
"more_with": "Mer med {{name}}",
|
"more_with": "Mer med {{name}}",
|
||||||
@@ -627,10 +753,21 @@
|
|||||||
"media_options": "Media Options",
|
"media_options": "Media Options",
|
||||||
"quality": "Kvalitet",
|
"quality": "Kvalitet",
|
||||||
"audio": "Lyd",
|
"audio": "Lyd",
|
||||||
"subtitles": "Undertittel",
|
"subtitles": {
|
||||||
|
"label": "Subtitle",
|
||||||
|
"none": "None",
|
||||||
|
"tracks": "Tracks"
|
||||||
|
},
|
||||||
"show_more": "Vis mer",
|
"show_more": "Vis mer",
|
||||||
"show_less": "Vis mindre",
|
"show_less": "Vis mindre",
|
||||||
|
"left": "left",
|
||||||
|
"more_info": "More Info",
|
||||||
|
"director": "Director",
|
||||||
|
"cast": "Cast",
|
||||||
|
"technical_details": "Technical Details",
|
||||||
"appeared_in": "Ble brukt i",
|
"appeared_in": "Ble brukt i",
|
||||||
|
"movies": "Movies",
|
||||||
|
"shows": "Shows",
|
||||||
"could_not_load_item": "Kan ikke laste inn produkt",
|
"could_not_load_item": "Kan ikke laste inn produkt",
|
||||||
"none": "Ingen",
|
"none": "Ingen",
|
||||||
"download": {
|
"download": {
|
||||||
@@ -641,7 +778,13 @@
|
|||||||
"download_x_item": "Last ned {{item_count}} Objekter",
|
"download_x_item": "Last ned {{item_count}} Objekter",
|
||||||
"download_unwatched_only": "Bare usette",
|
"download_unwatched_only": "Bare usette",
|
||||||
"download_button": "Nedlasting"
|
"download_button": "Nedlasting"
|
||||||
}
|
},
|
||||||
|
"mark_played": "Mark as Watched",
|
||||||
|
"mark_unplayed": "Mark as Unwatched",
|
||||||
|
"resume_playback": "Resume Playback",
|
||||||
|
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
|
||||||
|
"play_from_start": "Play from Start",
|
||||||
|
"continue_from": "Continue from {{time}}"
|
||||||
},
|
},
|
||||||
"live_tv": {
|
"live_tv": {
|
||||||
"next": "Neste",
|
"next": "Neste",
|
||||||
@@ -652,7 +795,18 @@
|
|||||||
"movies": "Filmer",
|
"movies": "Filmer",
|
||||||
"sports": "Sport",
|
"sports": "Sport",
|
||||||
"for_kids": "For barn",
|
"for_kids": "For barn",
|
||||||
"news": "Nyheter"
|
"news": "Nyheter",
|
||||||
|
"page_of": "Page {{current}} of {{total}}",
|
||||||
|
"no_programs": "No programs available",
|
||||||
|
"no_channels": "No channels available",
|
||||||
|
"tabs": {
|
||||||
|
"programs": "Programs",
|
||||||
|
"guide": "Guide",
|
||||||
|
"channels": "Channels",
|
||||||
|
"recordings": "Recordings",
|
||||||
|
"schedule": "Schedule",
|
||||||
|
"series": "Series"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"jellyseerr": {
|
"jellyseerr": {
|
||||||
"confirm": "Bekreft",
|
"confirm": "Bekreft",
|
||||||
@@ -697,6 +851,12 @@
|
|||||||
"decline": "Decline",
|
"decline": "Decline",
|
||||||
"requested_by": "Requested by {{user}}",
|
"requested_by": "Requested by {{user}}",
|
||||||
"unknown_user": "Unknown User",
|
"unknown_user": "Unknown User",
|
||||||
|
"select": "Select",
|
||||||
|
"request_all": "Request All",
|
||||||
|
"request_seasons": "Request Seasons",
|
||||||
|
"select_seasons": "Select Seasons",
|
||||||
|
"request_selected": "Request Selected",
|
||||||
|
"n_selected": "{{count}} selected",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"jellyseer_does_not_meet_requirements": "Seerr server oppfyller ikke minimumskravene til versjoner! Vennligst oppdater til minst 2.0.0",
|
"jellyseer_does_not_meet_requirements": "Seerr server oppfyller ikke minimumskravene til versjoner! Vennligst oppdater til minst 2.0.0",
|
||||||
"jellyseerr_test_failed": "Seerr-test mislyktes. Vennligst prøv på nytt.",
|
"jellyseerr_test_failed": "Seerr-test mislyktes. Vennligst prøv på nytt.",
|
||||||
@@ -716,7 +876,8 @@
|
|||||||
"search": "Søk",
|
"search": "Søk",
|
||||||
"library": "Bibliotek",
|
"library": "Bibliotek",
|
||||||
"custom_links": "Egendefinerte lenker",
|
"custom_links": "Egendefinerte lenker",
|
||||||
"favorites": "Favoritter"
|
"favorites": "Favoritter",
|
||||||
|
"settings": "Settings"
|
||||||
},
|
},
|
||||||
"music": {
|
"music": {
|
||||||
"title": "Music",
|
"title": "Music",
|
||||||
@@ -841,5 +1002,36 @@
|
|||||||
"show": "This show",
|
"show": "This show",
|
||||||
"all": "All media (default)"
|
"all": "All media (default)"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"companion_login": {
|
||||||
|
"title": "Pair with TV",
|
||||||
|
"align_qr": "Align the QR code within the frame",
|
||||||
|
"enter_code_manually": "Enter code manually",
|
||||||
|
"pairing_enter_credentials": "Enter credentials for TV",
|
||||||
|
"pairing_code_label": "Pairing code",
|
||||||
|
"server": "Server",
|
||||||
|
"authorize_button": "Authorize",
|
||||||
|
"authorizing": "Authorizing...",
|
||||||
|
"scan_again": "Scan Again",
|
||||||
|
"done": "Done",
|
||||||
|
"success_title": "Authorization Sent",
|
||||||
|
"pairing_tv_connecting": "The TV is connecting to your account",
|
||||||
|
"error_title": "Authorization Failed",
|
||||||
|
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
|
||||||
|
"error_generic": "Something went wrong. Please try again.",
|
||||||
|
"error_permission_denied": "Camera permission is required to scan QR codes.",
|
||||||
|
"login_as": "Log in as {{username}}?",
|
||||||
|
"on_server": "on {{server}}",
|
||||||
|
"use_different_user": "Use a different user",
|
||||||
|
"open_settings": "Open Settings"
|
||||||
|
},
|
||||||
|
"pairing": {
|
||||||
|
"pair_with_phone": "Pair with Phone",
|
||||||
|
"pair_with_phone_title": "Login TV",
|
||||||
|
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
|
||||||
|
"waiting_for_phone": "Waiting for phone...",
|
||||||
|
"scan_with_phone": "Scan with the Streamyfin app on your phone",
|
||||||
|
"logging_in": "Logging in...",
|
||||||
|
"logging_in_description": "Connecting to your server"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
"error_title": "Błąd",
|
"error_title": "Błąd",
|
||||||
"login_title": "Zaloguj się",
|
"login_title": "Zaloguj się",
|
||||||
"login_to_title": "Zaloguj się do",
|
"login_to_title": "Zaloguj się do",
|
||||||
|
"select_user": "Select a user to log in",
|
||||||
|
"add_user_to_login": "Add a user to log in",
|
||||||
|
"add_user": "Add User",
|
||||||
"username_placeholder": "Nazwa użytkownika",
|
"username_placeholder": "Nazwa użytkownika",
|
||||||
"password_placeholder": "Hasło",
|
"password_placeholder": "Hasło",
|
||||||
"login_button": "Zaloguj się",
|
"login_button": "Zaloguj się",
|
||||||
@@ -42,7 +45,13 @@
|
|||||||
"accounts_count": "{{count}} kont",
|
"accounts_count": "{{count}} kont",
|
||||||
"select_account": "Wybierz konto",
|
"select_account": "Wybierz konto",
|
||||||
"add_account": "Dodaj konto",
|
"add_account": "Dodaj konto",
|
||||||
"remove_account_description": "Spowoduje to usunięcie zapisanych danych logowania dla {{username}}."
|
"remove_account_description": "Spowoduje to usunięcie zapisanych danych logowania dla {{username}}.",
|
||||||
|
"remove_server": "Remove Server",
|
||||||
|
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
|
||||||
|
"select_your_server": "Select Your Server",
|
||||||
|
"add_server_to_get_started": "Add a server to get started",
|
||||||
|
"add_server": "Add Server",
|
||||||
|
"change_server": "Change Server"
|
||||||
},
|
},
|
||||||
"save_account": {
|
"save_account": {
|
||||||
"title": "Zapisz konto",
|
"title": "Zapisz konto",
|
||||||
@@ -86,6 +95,7 @@
|
|||||||
"oops": "Ups!",
|
"oops": "Ups!",
|
||||||
"error_message": "Coś poszło nie tak.\nWyloguj się i zaloguj ponownie.",
|
"error_message": "Coś poszło nie tak.\nWyloguj się i zaloguj ponownie.",
|
||||||
"continue_watching": "Kontynuuj oglądanie",
|
"continue_watching": "Kontynuuj oglądanie",
|
||||||
|
"continue": "Continue",
|
||||||
"next_up": "Następne w kolejce",
|
"next_up": "Następne w kolejce",
|
||||||
"continue_and_next_up": "Oglądaj dalej i Następne",
|
"continue_and_next_up": "Oglądaj dalej i Następne",
|
||||||
"recently_added_in": "Ostatnio dodano w {{libraryName}}",
|
"recently_added_in": "Ostatnio dodano w {{libraryName}}",
|
||||||
@@ -109,6 +119,12 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"settings_title": "Ustawienia",
|
"settings_title": "Ustawienia",
|
||||||
"log_out_button": "Wyloguj się",
|
"log_out_button": "Wyloguj się",
|
||||||
|
"switch_user": {
|
||||||
|
"title": "Switch User",
|
||||||
|
"account": "Account",
|
||||||
|
"switch_user": "Switch User on This Server",
|
||||||
|
"current": "current"
|
||||||
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "Kategorie"
|
"title": "Kategorie"
|
||||||
},
|
},
|
||||||
@@ -121,7 +137,16 @@
|
|||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "Wygląd",
|
"title": "Wygląd",
|
||||||
"merge_next_up_continue_watching": "Połącz Oglądaj dalej i Następne",
|
"merge_next_up_continue_watching": "Połącz Oglądaj dalej i Następne",
|
||||||
"hide_remote_session_button": "Ukryj przycisk Zdalnej Sesji"
|
"hide_remote_session_button": "Ukryj przycisk Zdalnej Sesji",
|
||||||
|
"show_home_backdrop": "Dynamic Home Backdrop",
|
||||||
|
"show_hero_carousel": "Hero Carousel",
|
||||||
|
"show_series_poster_on_episode": "Show Series Poster on Episodes",
|
||||||
|
"theme_music": "Theme Music",
|
||||||
|
"display_size": "Display Size",
|
||||||
|
"display_size_small": "Small",
|
||||||
|
"display_size_default": "Default",
|
||||||
|
"display_size_large": "Large",
|
||||||
|
"display_size_extra_large": "Extra Large"
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"title": "Network",
|
"title": "Network",
|
||||||
@@ -174,6 +199,22 @@
|
|||||||
"rewind_length": "Długość przewijania do tyłu",
|
"rewind_length": "Długość przewijania do tyłu",
|
||||||
"seconds_unit": "s"
|
"seconds_unit": "s"
|
||||||
},
|
},
|
||||||
|
"buffer": {
|
||||||
|
"title": "Buffer Settings",
|
||||||
|
"cache_mode": "Cache Mode",
|
||||||
|
"cache_auto": "Auto",
|
||||||
|
"cache_yes": "Enabled",
|
||||||
|
"cache_no": "Disabled",
|
||||||
|
"buffer_duration": "Buffer Duration",
|
||||||
|
"max_cache_size": "Max Cache Size",
|
||||||
|
"max_backward_cache": "Max Backward Cache"
|
||||||
|
},
|
||||||
|
"vo_driver": {
|
||||||
|
"title": "Video Output",
|
||||||
|
"vo_mode": "VO Driver",
|
||||||
|
"gpu_next": "gpu-next (Recommended)",
|
||||||
|
"gpu": "gpu"
|
||||||
|
},
|
||||||
"gesture_controls": {
|
"gesture_controls": {
|
||||||
"gesture_controls_title": "Sterowanie gestami",
|
"gesture_controls_title": "Sterowanie gestami",
|
||||||
"horizontal_swipe_skip": "Przesuń w poziomie, aby pominąć",
|
"horizontal_swipe_skip": "Przesuń w poziomie, aby pominąć",
|
||||||
@@ -256,7 +297,23 @@
|
|||||||
"subtitle_font": "Czcionka napisów",
|
"subtitle_font": "Czcionka napisów",
|
||||||
"ksplayer_title": "Ustawienia KSPlayer",
|
"ksplayer_title": "Ustawienia KSPlayer",
|
||||||
"hardware_decode": "Dekodowanie sprzętowe",
|
"hardware_decode": "Dekodowanie sprzętowe",
|
||||||
"hardware_decode_description": "Używaj akceleracji sprzętowej dla dekodowania wideo. Wyłącz, jeśli doświadczasz problemów z odtwarzaniem."
|
"hardware_decode_description": "Używaj akceleracji sprzętowej dla dekodowania wideo. Wyłącz, jeśli doświadczasz problemów z odtwarzaniem.",
|
||||||
|
"opensubtitles_title": "OpenSubtitles",
|
||||||
|
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
|
||||||
|
"opensubtitles_api_key": "API Key",
|
||||||
|
"opensubtitles_api_key_placeholder": "Enter API key...",
|
||||||
|
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
|
||||||
|
"mpv_subtitle_scale": "Subtitle Scale",
|
||||||
|
"mpv_subtitle_margin_y": "Vertical Margin",
|
||||||
|
"mpv_subtitle_align_x": "Horizontal Align",
|
||||||
|
"mpv_subtitle_align_y": "Vertical Align",
|
||||||
|
"align": {
|
||||||
|
"left": "Left",
|
||||||
|
"center": "Center",
|
||||||
|
"right": "Right",
|
||||||
|
"top": "Top",
|
||||||
|
"bottom": "Bottom"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"vlc_subtitles": {
|
"vlc_subtitles": {
|
||||||
"title": "Ustawienia napisów VLC",
|
"title": "Ustawienia napisów VLC",
|
||||||
@@ -406,7 +463,13 @@
|
|||||||
"music_cache_cleared": "Wyczyszczono bufor muzyki",
|
"music_cache_cleared": "Wyczyszczono bufor muzyki",
|
||||||
"delete_all_downloaded_songs": "Usuń wszystkie pobrane piosenki",
|
"delete_all_downloaded_songs": "Usuń wszystkie pobrane piosenki",
|
||||||
"downloaded_songs_size": "Pobrano {{size}}",
|
"downloaded_songs_size": "Pobrano {{size}}",
|
||||||
"downloaded_songs_deleted": "Usunięto pobrane piosenki"
|
"downloaded_songs_deleted": "Usunięto pobrane piosenki",
|
||||||
|
"clear_all_cache": "Clear All Cache",
|
||||||
|
"clear_all_cache_confirm": "Clear All Cache?",
|
||||||
|
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
|
||||||
|
"clear_all_cache_success": "Cache Cleared",
|
||||||
|
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
|
||||||
|
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "Wstęp",
|
"title": "Wstęp",
|
||||||
@@ -430,6 +493,21 @@
|
|||||||
"error_deleting_files": "Błąd podczas usuwania plików",
|
"error_deleting_files": "Błąd podczas usuwania plików",
|
||||||
"background_downloads_enabled": "Pobieranie w tle włączone",
|
"background_downloads_enabled": "Pobieranie w tle włączone",
|
||||||
"background_downloads_disabled": "Pobieranie w tle wyłączone"
|
"background_downloads_disabled": "Pobieranie w tle wyłączone"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"title": "Security",
|
||||||
|
"inactivity_timeout": {
|
||||||
|
"title": "Inactivity Timeout",
|
||||||
|
"description": "Auto logout after inactivity",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"1_minute": "1 minute",
|
||||||
|
"5_minutes": "5 minutes",
|
||||||
|
"15_minutes": "15 minutes",
|
||||||
|
"30_minutes": "30 minutes",
|
||||||
|
"1_hour": "1 hour",
|
||||||
|
"4_hours": "4 hours",
|
||||||
|
"24_hours": "24 hours"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -456,6 +534,7 @@
|
|||||||
"new_app_version_requires_re_download_description": "Nowa aktualizacja wymaga ponownego pobrania treści. Usuń wszystkie pobrane materiały i spróbuj ponownie.",
|
"new_app_version_requires_re_download_description": "Nowa aktualizacja wymaga ponownego pobrania treści. Usuń wszystkie pobrane materiały i spróbuj ponownie.",
|
||||||
"back": "Wstecz",
|
"back": "Wstecz",
|
||||||
"delete": "Usuń",
|
"delete": "Usuń",
|
||||||
|
"delete_download": "Delete Download",
|
||||||
"something_went_wrong": "Coś poszło nie tak",
|
"something_went_wrong": "Coś poszło nie tak",
|
||||||
"could_not_get_stream_url_from_jellyfin": "Nie udało się pobrać adresu URL transmisji z Jellyfin",
|
"could_not_get_stream_url_from_jellyfin": "Nie udało się pobrać adresu URL transmisji z Jellyfin",
|
||||||
"eta": "Szacowany czas: {{eta}}",
|
"eta": "Szacowany czas: {{eta}}",
|
||||||
@@ -492,22 +571,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"no_results": "No Results",
|
||||||
"select": "Wybierz",
|
"select": "Wybierz",
|
||||||
"no_trailer_available": "Brak dostępnego zwiastunu",
|
"no_trailer_available": "Brak dostępnego zwiastunu",
|
||||||
"video": "Wideo",
|
"video": "Wideo",
|
||||||
"audio": "Dźwięk",
|
"audio": "Dźwięk",
|
||||||
"subtitle": "Napisy",
|
"subtitle": "Napisy",
|
||||||
"play": "Odtwórz",
|
"play": "Odtwórz",
|
||||||
|
"mark_as_played": "Mark as Played",
|
||||||
|
"mark_as_not_played": "Mark as not Played",
|
||||||
"none": "Nic",
|
"none": "Nic",
|
||||||
"track": "Utwór",
|
"track": "Utwór",
|
||||||
"cancel": "Anuluj",
|
"cancel": "Anuluj",
|
||||||
|
"stop": "Stop",
|
||||||
"delete": "Usuń",
|
"delete": "Usuń",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"remove": "Usuń",
|
"remove": "Usuń",
|
||||||
"next": "Następne",
|
"next": "Następne",
|
||||||
"back": "Poprzednie",
|
"back": "Poprzednie",
|
||||||
"continue": "Kontynuuj",
|
"continue": "Kontynuuj",
|
||||||
"verifying": "Weryfikacja..."
|
"verifying": "Weryfikacja...",
|
||||||
|
"login": "Login",
|
||||||
|
"refresh": "Refresh"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"search": "Szukaj...",
|
"search": "Szukaj...",
|
||||||
@@ -556,6 +641,7 @@
|
|||||||
"movies": "filmy",
|
"movies": "filmy",
|
||||||
"series": "seriale",
|
"series": "seriale",
|
||||||
"boxsets": "zestawy",
|
"boxsets": "zestawy",
|
||||||
|
"playlists": "Playlists",
|
||||||
"items": "elementy"
|
"items": "elementy"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
@@ -566,7 +652,8 @@
|
|||||||
"poster": "Plakat",
|
"poster": "Plakat",
|
||||||
"cover": "Okładka",
|
"cover": "Okładka",
|
||||||
"show_titles": "Pokaż tytuły",
|
"show_titles": "Pokaż tytuły",
|
||||||
"show_stats": "Pokaż statystyki"
|
"show_stats": "Pokaż statystyki",
|
||||||
|
"options_title": "Options"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"genres": "Gatunki",
|
"genres": "Gatunki",
|
||||||
@@ -574,7 +661,11 @@
|
|||||||
"sort_by": "Sortuj według",
|
"sort_by": "Sortuj według",
|
||||||
"filter_by": "Filtruj po",
|
"filter_by": "Filtruj po",
|
||||||
"sort_order": "Kolejność sortowania",
|
"sort_order": "Kolejność sortowania",
|
||||||
"tags": "Tagi"
|
"tags": "Tagi",
|
||||||
|
"all": "All",
|
||||||
|
"reset": "Reset",
|
||||||
|
"asc": "Ascending",
|
||||||
|
"desc": "Descending"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"favorites": {
|
"favorites": {
|
||||||
@@ -591,6 +682,8 @@
|
|||||||
"no_links": "Brak odnośników"
|
"no_links": "Brak odnośników"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
|
"live": "LIVE",
|
||||||
|
"mpv_player_title": "MPV Player",
|
||||||
"error": "Błąd",
|
"error": "Błąd",
|
||||||
"failed_to_get_stream_url": "Nie udało się pobrać adresu strumienia",
|
"failed_to_get_stream_url": "Nie udało się pobrać adresu strumienia",
|
||||||
"an_error_occured_while_playing_the_video": "Wystąpił błąd podczas odtwarzania wideo. Sprawdź logi w ustawieniach.",
|
"an_error_occured_while_playing_the_video": "Wystąpił błąd podczas odtwarzania wideo. Sprawdź logi w ustawieniach.",
|
||||||
@@ -608,7 +701,35 @@
|
|||||||
"downloaded_file_message": "Chcesz odtworzyć pobrany plik?",
|
"downloaded_file_message": "Chcesz odtworzyć pobrany plik?",
|
||||||
"downloaded_file_yes": "Tak",
|
"downloaded_file_yes": "Tak",
|
||||||
"downloaded_file_no": "Nie",
|
"downloaded_file_no": "Nie",
|
||||||
"downloaded_file_cancel": "Anuluj"
|
"downloaded_file_cancel": "Anuluj",
|
||||||
|
"swipe_down_settings": "Swipe down for settings",
|
||||||
|
"ends_at": "Ends at {{time}}",
|
||||||
|
"search_subtitles": "Search Subtitles",
|
||||||
|
"subtitle_tracks": "Tracks",
|
||||||
|
"subtitle_search": "Search & Download",
|
||||||
|
"download": "Download",
|
||||||
|
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
|
||||||
|
"using_jellyfin_server": "Using Jellyfin Server",
|
||||||
|
"language": "Language",
|
||||||
|
"results": "Results",
|
||||||
|
"searching": "Searching...",
|
||||||
|
"search_failed": "Search failed",
|
||||||
|
"no_subtitle_provider": "No subtitle provider configured on server",
|
||||||
|
"no_subtitles_found": "No subtitles found",
|
||||||
|
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
|
||||||
|
"settings": "Settings",
|
||||||
|
"skip_intro": "Skip Intro",
|
||||||
|
"skip_credits": "Skip Credits",
|
||||||
|
"stopPlayback": "Stop Playback",
|
||||||
|
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
|
||||||
|
"stopPlayingConfirm": "Are you sure you want to stop playback?",
|
||||||
|
"downloaded": "Downloaded"
|
||||||
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Następne",
|
"next_up": "Następne",
|
||||||
@@ -617,6 +738,11 @@
|
|||||||
"series": "Serial",
|
"series": "Serial",
|
||||||
"seasons": "Sezony",
|
"seasons": "Sezony",
|
||||||
"season": "Sezon",
|
"season": "Sezon",
|
||||||
|
"from_this_series": "From This Series",
|
||||||
|
"more_from_this_season": "More from this Season",
|
||||||
|
"view_series": "View Series",
|
||||||
|
"view_season": "View Season",
|
||||||
|
"select_season": "Select Season",
|
||||||
"no_episodes_for_this_season": "Brak odcinków w tym sezonie",
|
"no_episodes_for_this_season": "Brak odcinków w tym sezonie",
|
||||||
"overview": "Opis",
|
"overview": "Opis",
|
||||||
"more_with": "Więcej z {{name}}",
|
"more_with": "Więcej z {{name}}",
|
||||||
@@ -627,10 +753,21 @@
|
|||||||
"media_options": "Ustawienia mediów",
|
"media_options": "Ustawienia mediów",
|
||||||
"quality": "Jakość",
|
"quality": "Jakość",
|
||||||
"audio": "Dźwięk",
|
"audio": "Dźwięk",
|
||||||
"subtitles": "Napisy",
|
"subtitles": {
|
||||||
|
"label": "Subtitle",
|
||||||
|
"none": "None",
|
||||||
|
"tracks": "Tracks"
|
||||||
|
},
|
||||||
"show_more": "Pokaż więcej",
|
"show_more": "Pokaż więcej",
|
||||||
"show_less": "Pokaż mniej",
|
"show_less": "Pokaż mniej",
|
||||||
|
"left": "left",
|
||||||
|
"more_info": "More Info",
|
||||||
|
"director": "Director",
|
||||||
|
"cast": "Cast",
|
||||||
|
"technical_details": "Technical Details",
|
||||||
"appeared_in": "Wystąpił w",
|
"appeared_in": "Wystąpił w",
|
||||||
|
"movies": "Movies",
|
||||||
|
"shows": "Shows",
|
||||||
"could_not_load_item": "Nie udało się wczytać elementu",
|
"could_not_load_item": "Nie udało się wczytać elementu",
|
||||||
"none": "Brak",
|
"none": "Brak",
|
||||||
"download": {
|
"download": {
|
||||||
@@ -641,7 +778,13 @@
|
|||||||
"download_x_item": "Pobierz {{item_count}} elementów",
|
"download_x_item": "Pobierz {{item_count}} elementów",
|
||||||
"download_unwatched_only": "Tylko nieobejrzane",
|
"download_unwatched_only": "Tylko nieobejrzane",
|
||||||
"download_button": "Pobierz"
|
"download_button": "Pobierz"
|
||||||
}
|
},
|
||||||
|
"mark_played": "Mark as Watched",
|
||||||
|
"mark_unplayed": "Mark as Unwatched",
|
||||||
|
"resume_playback": "Resume Playback",
|
||||||
|
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
|
||||||
|
"play_from_start": "Play from Start",
|
||||||
|
"continue_from": "Continue from {{time}}"
|
||||||
},
|
},
|
||||||
"live_tv": {
|
"live_tv": {
|
||||||
"next": "Następny",
|
"next": "Następny",
|
||||||
@@ -652,7 +795,18 @@
|
|||||||
"movies": "Filmy",
|
"movies": "Filmy",
|
||||||
"sports": "Sport",
|
"sports": "Sport",
|
||||||
"for_kids": "Dla dzieci",
|
"for_kids": "Dla dzieci",
|
||||||
"news": "Wiadomości"
|
"news": "Wiadomości",
|
||||||
|
"page_of": "Page {{current}} of {{total}}",
|
||||||
|
"no_programs": "No programs available",
|
||||||
|
"no_channels": "No channels available",
|
||||||
|
"tabs": {
|
||||||
|
"programs": "Programs",
|
||||||
|
"guide": "Guide",
|
||||||
|
"channels": "Channels",
|
||||||
|
"recordings": "Recordings",
|
||||||
|
"schedule": "Schedule",
|
||||||
|
"series": "Series"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"jellyseerr": {
|
"jellyseerr": {
|
||||||
"confirm": "Potwierdź",
|
"confirm": "Potwierdź",
|
||||||
@@ -697,6 +851,12 @@
|
|||||||
"decline": "Odrzuć",
|
"decline": "Odrzuć",
|
||||||
"requested_by": "Poproszone przez {{user}}",
|
"requested_by": "Poproszone przez {{user}}",
|
||||||
"unknown_user": "Nieznany użytkownik",
|
"unknown_user": "Nieznany użytkownik",
|
||||||
|
"select": "Select",
|
||||||
|
"request_all": "Request All",
|
||||||
|
"request_seasons": "Request Seasons",
|
||||||
|
"select_seasons": "Select Seasons",
|
||||||
|
"request_selected": "Request Selected",
|
||||||
|
"n_selected": "{{count}} selected",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"jellyseer_does_not_meet_requirements": "Serwer Jellyseerr nie spełnia minimalnych wymagań wersji! Zaktualizuj go co najmniej do wersji 2.0.0",
|
"jellyseer_does_not_meet_requirements": "Serwer Jellyseerr nie spełnia minimalnych wymagań wersji! Zaktualizuj go co najmniej do wersji 2.0.0",
|
||||||
"jellyseerr_test_failed": "Test Jellyseerr nie powiódł się. Spróbuj ponownie.",
|
"jellyseerr_test_failed": "Test Jellyseerr nie powiódł się. Spróbuj ponownie.",
|
||||||
@@ -716,7 +876,8 @@
|
|||||||
"search": "Szukaj",
|
"search": "Szukaj",
|
||||||
"library": "Biblioteka",
|
"library": "Biblioteka",
|
||||||
"custom_links": "Niestandardowe odnośniki",
|
"custom_links": "Niestandardowe odnośniki",
|
||||||
"favorites": "Ulubione"
|
"favorites": "Ulubione",
|
||||||
|
"settings": "Settings"
|
||||||
},
|
},
|
||||||
"music": {
|
"music": {
|
||||||
"title": "Muzyka",
|
"title": "Muzyka",
|
||||||
@@ -841,5 +1002,36 @@
|
|||||||
"show": "Ten odcinek",
|
"show": "Ten odcinek",
|
||||||
"all": "Wszystkie media (domyślne)"
|
"all": "Wszystkie media (domyślne)"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"companion_login": {
|
||||||
|
"title": "Pair with TV",
|
||||||
|
"align_qr": "Align the QR code within the frame",
|
||||||
|
"enter_code_manually": "Enter code manually",
|
||||||
|
"pairing_enter_credentials": "Enter credentials for TV",
|
||||||
|
"pairing_code_label": "Pairing code",
|
||||||
|
"server": "Server",
|
||||||
|
"authorize_button": "Authorize",
|
||||||
|
"authorizing": "Authorizing...",
|
||||||
|
"scan_again": "Scan Again",
|
||||||
|
"done": "Done",
|
||||||
|
"success_title": "Authorization Sent",
|
||||||
|
"pairing_tv_connecting": "The TV is connecting to your account",
|
||||||
|
"error_title": "Authorization Failed",
|
||||||
|
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
|
||||||
|
"error_generic": "Something went wrong. Please try again.",
|
||||||
|
"error_permission_denied": "Camera permission is required to scan QR codes.",
|
||||||
|
"login_as": "Log in as {{username}}?",
|
||||||
|
"on_server": "on {{server}}",
|
||||||
|
"use_different_user": "Use a different user",
|
||||||
|
"open_settings": "Open Settings"
|
||||||
|
},
|
||||||
|
"pairing": {
|
||||||
|
"pair_with_phone": "Pair with Phone",
|
||||||
|
"pair_with_phone_title": "Login TV",
|
||||||
|
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
|
||||||
|
"waiting_for_phone": "Waiting for phone...",
|
||||||
|
"scan_with_phone": "Scan with the Streamyfin app on your phone",
|
||||||
|
"logging_in": "Logging in...",
|
||||||
|
"logging_in_description": "Connecting to your server"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
"error_title": "Erro",
|
"error_title": "Erro",
|
||||||
"login_title": "Iniciar sessão",
|
"login_title": "Iniciar sessão",
|
||||||
"login_to_title": "Iniciar sessão em",
|
"login_to_title": "Iniciar sessão em",
|
||||||
|
"select_user": "Select a user to log in",
|
||||||
|
"add_user_to_login": "Add a user to log in",
|
||||||
|
"add_user": "Add User",
|
||||||
"username_placeholder": "Usuário",
|
"username_placeholder": "Usuário",
|
||||||
"password_placeholder": "Palavra-passe",
|
"password_placeholder": "Palavra-passe",
|
||||||
"login_button": "Iniciar sessão",
|
"login_button": "Iniciar sessão",
|
||||||
@@ -42,7 +45,13 @@
|
|||||||
"accounts_count": "{{count}} accounts",
|
"accounts_count": "{{count}} accounts",
|
||||||
"select_account": "Select Account",
|
"select_account": "Select Account",
|
||||||
"add_account": "Add Account",
|
"add_account": "Add Account",
|
||||||
"remove_account_description": "This will remove the saved credentials for {{username}}."
|
"remove_account_description": "This will remove the saved credentials for {{username}}.",
|
||||||
|
"remove_server": "Remove Server",
|
||||||
|
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
|
||||||
|
"select_your_server": "Select Your Server",
|
||||||
|
"add_server_to_get_started": "Add a server to get started",
|
||||||
|
"add_server": "Add Server",
|
||||||
|
"change_server": "Change Server"
|
||||||
},
|
},
|
||||||
"save_account": {
|
"save_account": {
|
||||||
"title": "Save Account",
|
"title": "Save Account",
|
||||||
@@ -86,6 +95,7 @@
|
|||||||
"oops": "Opa!",
|
"oops": "Opa!",
|
||||||
"error_message": "Algo deu errado.\nPor favor, saia e entre novamente.",
|
"error_message": "Algo deu errado.\nPor favor, saia e entre novamente.",
|
||||||
"continue_watching": "Continuar assistindo",
|
"continue_watching": "Continuar assistindo",
|
||||||
|
"continue": "Continue",
|
||||||
"next_up": "A Seguir",
|
"next_up": "A Seguir",
|
||||||
"continue_and_next_up": "Continuar e Próximo",
|
"continue_and_next_up": "Continuar e Próximo",
|
||||||
"recently_added_in": "Adicionado recentemente em {{libraryName}}",
|
"recently_added_in": "Adicionado recentemente em {{libraryName}}",
|
||||||
@@ -109,6 +119,12 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"settings_title": "Confirgurações",
|
"settings_title": "Confirgurações",
|
||||||
"log_out_button": "Encerrar Sessão",
|
"log_out_button": "Encerrar Sessão",
|
||||||
|
"switch_user": {
|
||||||
|
"title": "Switch User",
|
||||||
|
"account": "Account",
|
||||||
|
"switch_user": "Switch User on This Server",
|
||||||
|
"current": "current"
|
||||||
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "Categorias"
|
"title": "Categorias"
|
||||||
},
|
},
|
||||||
@@ -121,7 +137,16 @@
|
|||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "Aparência",
|
"title": "Aparência",
|
||||||
"merge_next_up_continue_watching": "Mesclar Continuar Assistindo e Próximo",
|
"merge_next_up_continue_watching": "Mesclar Continuar Assistindo e Próximo",
|
||||||
"hide_remote_session_button": "Esconder botão de sessão remota"
|
"hide_remote_session_button": "Esconder botão de sessão remota",
|
||||||
|
"show_home_backdrop": "Dynamic Home Backdrop",
|
||||||
|
"show_hero_carousel": "Hero Carousel",
|
||||||
|
"show_series_poster_on_episode": "Show Series Poster on Episodes",
|
||||||
|
"theme_music": "Theme Music",
|
||||||
|
"display_size": "Display Size",
|
||||||
|
"display_size_small": "Small",
|
||||||
|
"display_size_default": "Default",
|
||||||
|
"display_size_large": "Large",
|
||||||
|
"display_size_extra_large": "Extra Large"
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"title": "Network",
|
"title": "Network",
|
||||||
@@ -174,6 +199,22 @@
|
|||||||
"rewind_length": "Comprimento de Retroceder",
|
"rewind_length": "Comprimento de Retroceder",
|
||||||
"seconds_unit": "s"
|
"seconds_unit": "s"
|
||||||
},
|
},
|
||||||
|
"buffer": {
|
||||||
|
"title": "Buffer Settings",
|
||||||
|
"cache_mode": "Cache Mode",
|
||||||
|
"cache_auto": "Auto",
|
||||||
|
"cache_yes": "Enabled",
|
||||||
|
"cache_no": "Disabled",
|
||||||
|
"buffer_duration": "Buffer Duration",
|
||||||
|
"max_cache_size": "Max Cache Size",
|
||||||
|
"max_backward_cache": "Max Backward Cache"
|
||||||
|
},
|
||||||
|
"vo_driver": {
|
||||||
|
"title": "Video Output",
|
||||||
|
"vo_mode": "VO Driver",
|
||||||
|
"gpu_next": "gpu-next (Recommended)",
|
||||||
|
"gpu": "gpu"
|
||||||
|
},
|
||||||
"gesture_controls": {
|
"gesture_controls": {
|
||||||
"gesture_controls_title": "Controles de Gestos",
|
"gesture_controls_title": "Controles de Gestos",
|
||||||
"horizontal_swipe_skip": "Deslizar horizontalmente para pular",
|
"horizontal_swipe_skip": "Deslizar horizontalmente para pular",
|
||||||
@@ -256,7 +297,23 @@
|
|||||||
"subtitle_font": "Fonte da legenda",
|
"subtitle_font": "Fonte da legenda",
|
||||||
"ksplayer_title": "Configurações do KSPlayer",
|
"ksplayer_title": "Configurações do KSPlayer",
|
||||||
"hardware_decode": "Decodificação por hardware",
|
"hardware_decode": "Decodificação por hardware",
|
||||||
"hardware_decode_description": "Use aceleração de hardware para decodificação de vídeo. Desative se você tiver problemas de reprodução."
|
"hardware_decode_description": "Use aceleração de hardware para decodificação de vídeo. Desative se você tiver problemas de reprodução.",
|
||||||
|
"opensubtitles_title": "OpenSubtitles",
|
||||||
|
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
|
||||||
|
"opensubtitles_api_key": "API Key",
|
||||||
|
"opensubtitles_api_key_placeholder": "Enter API key...",
|
||||||
|
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
|
||||||
|
"mpv_subtitle_scale": "Subtitle Scale",
|
||||||
|
"mpv_subtitle_margin_y": "Vertical Margin",
|
||||||
|
"mpv_subtitle_align_x": "Horizontal Align",
|
||||||
|
"mpv_subtitle_align_y": "Vertical Align",
|
||||||
|
"align": {
|
||||||
|
"left": "Left",
|
||||||
|
"center": "Center",
|
||||||
|
"right": "Right",
|
||||||
|
"top": "Top",
|
||||||
|
"bottom": "Bottom"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"vlc_subtitles": {
|
"vlc_subtitles": {
|
||||||
"title": "VLC Subtitle Settings",
|
"title": "VLC Subtitle Settings",
|
||||||
@@ -406,7 +463,13 @@
|
|||||||
"music_cache_cleared": "Cache de música limpo",
|
"music_cache_cleared": "Cache de música limpo",
|
||||||
"delete_all_downloaded_songs": "Excluir todas as músicas baixadas",
|
"delete_all_downloaded_songs": "Excluir todas as músicas baixadas",
|
||||||
"downloaded_songs_size": "{{size}} baixado",
|
"downloaded_songs_size": "{{size}} baixado",
|
||||||
"downloaded_songs_deleted": "Músicas baixadas excluídas"
|
"downloaded_songs_deleted": "Músicas baixadas excluídas",
|
||||||
|
"clear_all_cache": "Clear All Cache",
|
||||||
|
"clear_all_cache_confirm": "Clear All Cache?",
|
||||||
|
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
|
||||||
|
"clear_all_cache_success": "Cache Cleared",
|
||||||
|
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
|
||||||
|
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "Intro",
|
"title": "Intro",
|
||||||
@@ -430,6 +493,21 @@
|
|||||||
"error_deleting_files": "Erro ao excluir arquivos",
|
"error_deleting_files": "Erro ao excluir arquivos",
|
||||||
"background_downloads_enabled": "Downloads em segundo plano ativados",
|
"background_downloads_enabled": "Downloads em segundo plano ativados",
|
||||||
"background_downloads_disabled": "Downloads em segundo plano desativados"
|
"background_downloads_disabled": "Downloads em segundo plano desativados"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"title": "Security",
|
||||||
|
"inactivity_timeout": {
|
||||||
|
"title": "Inactivity Timeout",
|
||||||
|
"description": "Auto logout after inactivity",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"1_minute": "1 minute",
|
||||||
|
"5_minutes": "5 minutes",
|
||||||
|
"15_minutes": "15 minutes",
|
||||||
|
"30_minutes": "30 minutes",
|
||||||
|
"1_hour": "1 hour",
|
||||||
|
"4_hours": "4 hours",
|
||||||
|
"24_hours": "24 hours"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -456,6 +534,7 @@
|
|||||||
"new_app_version_requires_re_download_description": "A nova atualização requer que o conteúdo seja baixado novamente. Por favor, remova todo o conteúdo baixado e tente novamente.",
|
"new_app_version_requires_re_download_description": "A nova atualização requer que o conteúdo seja baixado novamente. Por favor, remova todo o conteúdo baixado e tente novamente.",
|
||||||
"back": "Anterior",
|
"back": "Anterior",
|
||||||
"delete": "excluir",
|
"delete": "excluir",
|
||||||
|
"delete_download": "Delete Download",
|
||||||
"something_went_wrong": "Ocorreu Um Erro",
|
"something_went_wrong": "Ocorreu Um Erro",
|
||||||
"could_not_get_stream_url_from_jellyfin": "Não foi possível obter o URL de transmissão do Jellyfin",
|
"could_not_get_stream_url_from_jellyfin": "Não foi possível obter o URL de transmissão do Jellyfin",
|
||||||
"eta": "ETA {{eta}}",
|
"eta": "ETA {{eta}}",
|
||||||
@@ -492,22 +571,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"no_results": "No Results",
|
||||||
"select": "Selecionar",
|
"select": "Selecionar",
|
||||||
"no_trailer_available": "Nenhum trailer disponível",
|
"no_trailer_available": "Nenhum trailer disponível",
|
||||||
"video": "Vídeo",
|
"video": "Vídeo",
|
||||||
"audio": "Áudio",
|
"audio": "Áudio",
|
||||||
"subtitle": "Legenda",
|
"subtitle": "Legenda",
|
||||||
"play": "Reproduzir",
|
"play": "Reproduzir",
|
||||||
|
"mark_as_played": "Mark as Played",
|
||||||
|
"mark_as_not_played": "Mark as not Played",
|
||||||
"none": "Nenhum",
|
"none": "Nenhum",
|
||||||
"track": "Faixa",
|
"track": "Faixa",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
|
"stop": "Stop",
|
||||||
"delete": "Apagar",
|
"delete": "Apagar",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"remove": "Remover",
|
"remove": "Remover",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"verifying": "Verifying..."
|
"verifying": "Verifying...",
|
||||||
|
"login": "Login",
|
||||||
|
"refresh": "Refresh"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"search": "Buscar...",
|
"search": "Buscar...",
|
||||||
@@ -556,6 +641,7 @@
|
|||||||
"movies": "Filmes",
|
"movies": "Filmes",
|
||||||
"series": "Série",
|
"series": "Série",
|
||||||
"boxsets": "Conjuntos de caixas",
|
"boxsets": "Conjuntos de caixas",
|
||||||
|
"playlists": "Playlists",
|
||||||
"items": "itens"
|
"items": "itens"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
@@ -566,7 +652,8 @@
|
|||||||
"poster": "Cartaz",
|
"poster": "Cartaz",
|
||||||
"cover": "Capa",
|
"cover": "Capa",
|
||||||
"show_titles": "Mostrar Títulos",
|
"show_titles": "Mostrar Títulos",
|
||||||
"show_stats": "Mostrar estatísticas"
|
"show_stats": "Mostrar estatísticas",
|
||||||
|
"options_title": "Options"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"genres": "Genres",
|
"genres": "Genres",
|
||||||
@@ -574,7 +661,11 @@
|
|||||||
"sort_by": "Classificar por",
|
"sort_by": "Classificar por",
|
||||||
"filter_by": "Filtrar Por",
|
"filter_by": "Filtrar Por",
|
||||||
"sort_order": "Ordem de classificação",
|
"sort_order": "Ordem de classificação",
|
||||||
"tags": "Etiquetas"
|
"tags": "Etiquetas",
|
||||||
|
"all": "All",
|
||||||
|
"reset": "Reset",
|
||||||
|
"asc": "Ascending",
|
||||||
|
"desc": "Descending"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"favorites": {
|
"favorites": {
|
||||||
@@ -591,6 +682,8 @@
|
|||||||
"no_links": "Sem links"
|
"no_links": "Sem links"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
|
"live": "LIVE",
|
||||||
|
"mpv_player_title": "MPV Player",
|
||||||
"error": "ERRO",
|
"error": "ERRO",
|
||||||
"failed_to_get_stream_url": "Falha ao obter a URL de transmissão",
|
"failed_to_get_stream_url": "Falha ao obter a URL de transmissão",
|
||||||
"an_error_occured_while_playing_the_video": "Ocorreu um erro ao reproduzir o vídeo. Verifique os logs nas configurações.",
|
"an_error_occured_while_playing_the_video": "Ocorreu um erro ao reproduzir o vídeo. Verifique os logs nas configurações.",
|
||||||
@@ -608,7 +701,35 @@
|
|||||||
"downloaded_file_message": "Você quer reproduzir o arquivo baixado?",
|
"downloaded_file_message": "Você quer reproduzir o arquivo baixado?",
|
||||||
"downloaded_file_yes": "SIm",
|
"downloaded_file_yes": "SIm",
|
||||||
"downloaded_file_no": "Não",
|
"downloaded_file_no": "Não",
|
||||||
"downloaded_file_cancel": "Cancelar"
|
"downloaded_file_cancel": "Cancelar",
|
||||||
|
"swipe_down_settings": "Swipe down for settings",
|
||||||
|
"ends_at": "Ends at {{time}}",
|
||||||
|
"search_subtitles": "Search Subtitles",
|
||||||
|
"subtitle_tracks": "Tracks",
|
||||||
|
"subtitle_search": "Search & Download",
|
||||||
|
"download": "Download",
|
||||||
|
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
|
||||||
|
"using_jellyfin_server": "Using Jellyfin Server",
|
||||||
|
"language": "Language",
|
||||||
|
"results": "Results",
|
||||||
|
"searching": "Searching...",
|
||||||
|
"search_failed": "Search failed",
|
||||||
|
"no_subtitle_provider": "No subtitle provider configured on server",
|
||||||
|
"no_subtitles_found": "No subtitles found",
|
||||||
|
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
|
||||||
|
"settings": "Settings",
|
||||||
|
"skip_intro": "Skip Intro",
|
||||||
|
"skip_credits": "Skip Credits",
|
||||||
|
"stopPlayback": "Stop Playback",
|
||||||
|
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
|
||||||
|
"stopPlayingConfirm": "Are you sure you want to stop playback?",
|
||||||
|
"downloaded": "Downloaded"
|
||||||
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "A Seguir",
|
"next_up": "A Seguir",
|
||||||
@@ -617,6 +738,11 @@
|
|||||||
"series": "Série",
|
"series": "Série",
|
||||||
"seasons": "Estações",
|
"seasons": "Estações",
|
||||||
"season": "Temporada",
|
"season": "Temporada",
|
||||||
|
"from_this_series": "From This Series",
|
||||||
|
"more_from_this_season": "More from this Season",
|
||||||
|
"view_series": "View Series",
|
||||||
|
"view_season": "View Season",
|
||||||
|
"select_season": "Select Season",
|
||||||
"no_episodes_for_this_season": "Não há episódios para esta temporada",
|
"no_episodes_for_this_season": "Não há episódios para esta temporada",
|
||||||
"overview": "Geral",
|
"overview": "Geral",
|
||||||
"more_with": "Mais com {{name}}",
|
"more_with": "Mais com {{name}}",
|
||||||
@@ -627,10 +753,21 @@
|
|||||||
"media_options": "Opções de Mídia",
|
"media_options": "Opções de Mídia",
|
||||||
"quality": "Qualidade",
|
"quality": "Qualidade",
|
||||||
"audio": "Áudio",
|
"audio": "Áudio",
|
||||||
"subtitles": "Legenda",
|
"subtitles": {
|
||||||
|
"label": "Subtitle",
|
||||||
|
"none": "None",
|
||||||
|
"tracks": "Tracks"
|
||||||
|
},
|
||||||
"show_more": "Mostrar mais",
|
"show_more": "Mostrar mais",
|
||||||
"show_less": "Mostrar menos",
|
"show_less": "Mostrar menos",
|
||||||
|
"left": "left",
|
||||||
|
"more_info": "More Info",
|
||||||
|
"director": "Director",
|
||||||
|
"cast": "Cast",
|
||||||
|
"technical_details": "Technical Details",
|
||||||
"appeared_in": "Aparece em",
|
"appeared_in": "Aparece em",
|
||||||
|
"movies": "Movies",
|
||||||
|
"shows": "Shows",
|
||||||
"could_not_load_item": "Não foi possível carregar o item",
|
"could_not_load_item": "Não foi possível carregar o item",
|
||||||
"none": "Nenhuma",
|
"none": "Nenhuma",
|
||||||
"download": {
|
"download": {
|
||||||
@@ -641,7 +778,13 @@
|
|||||||
"download_x_item": "Baixar itens de {{item_count}}",
|
"download_x_item": "Baixar itens de {{item_count}}",
|
||||||
"download_unwatched_only": "Apenas não assistidos",
|
"download_unwatched_only": "Apenas não assistidos",
|
||||||
"download_button": "BAIXAR"
|
"download_button": "BAIXAR"
|
||||||
}
|
},
|
||||||
|
"mark_played": "Mark as Watched",
|
||||||
|
"mark_unplayed": "Mark as Unwatched",
|
||||||
|
"resume_playback": "Resume Playback",
|
||||||
|
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
|
||||||
|
"play_from_start": "Play from Start",
|
||||||
|
"continue_from": "Continue from {{time}}"
|
||||||
},
|
},
|
||||||
"live_tv": {
|
"live_tv": {
|
||||||
"next": "Próximo",
|
"next": "Próximo",
|
||||||
@@ -652,7 +795,18 @@
|
|||||||
"movies": "Filmes",
|
"movies": "Filmes",
|
||||||
"sports": "Esportes",
|
"sports": "Esportes",
|
||||||
"for_kids": "Para crianças",
|
"for_kids": "Para crianças",
|
||||||
"news": "Notícias"
|
"news": "Notícias",
|
||||||
|
"page_of": "Page {{current}} of {{total}}",
|
||||||
|
"no_programs": "No programs available",
|
||||||
|
"no_channels": "No channels available",
|
||||||
|
"tabs": {
|
||||||
|
"programs": "Programs",
|
||||||
|
"guide": "Guide",
|
||||||
|
"channels": "Channels",
|
||||||
|
"recordings": "Recordings",
|
||||||
|
"schedule": "Schedule",
|
||||||
|
"series": "Series"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"jellyseerr": {
|
"jellyseerr": {
|
||||||
"confirm": "Confirmar",
|
"confirm": "Confirmar",
|
||||||
@@ -697,6 +851,12 @@
|
|||||||
"decline": "Declinar",
|
"decline": "Declinar",
|
||||||
"requested_by": "Solicitado por {{user}}",
|
"requested_by": "Solicitado por {{user}}",
|
||||||
"unknown_user": "Usuário desconhecido",
|
"unknown_user": "Usuário desconhecido",
|
||||||
|
"select": "Select",
|
||||||
|
"request_all": "Request All",
|
||||||
|
"request_seasons": "Request Seasons",
|
||||||
|
"select_seasons": "Select Seasons",
|
||||||
|
"request_selected": "Request Selected",
|
||||||
|
"n_selected": "{{count}} selected",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"jellyseer_does_not_meet_requirements": "O servidor do Seerr não atende ao mínimo aos requisitos de versão! Por favor, atualize para pelo menos 2.0.0",
|
"jellyseer_does_not_meet_requirements": "O servidor do Seerr não atende ao mínimo aos requisitos de versão! Por favor, atualize para pelo menos 2.0.0",
|
||||||
"jellyseerr_test_failed": "Falha no teste do senhor. Por favor, tente novamente.",
|
"jellyseerr_test_failed": "Falha no teste do senhor. Por favor, tente novamente.",
|
||||||
@@ -716,7 +876,8 @@
|
|||||||
"search": "Pesquisa",
|
"search": "Pesquisa",
|
||||||
"library": "Biblioteca",
|
"library": "Biblioteca",
|
||||||
"custom_links": "Links personalizados",
|
"custom_links": "Links personalizados",
|
||||||
"favorites": "Atalhos"
|
"favorites": "Atalhos",
|
||||||
|
"settings": "Settings"
|
||||||
},
|
},
|
||||||
"music": {
|
"music": {
|
||||||
"title": "Música",
|
"title": "Música",
|
||||||
@@ -841,5 +1002,36 @@
|
|||||||
"show": "Esta série",
|
"show": "Esta série",
|
||||||
"all": "Todas as mídias (Padrão)"
|
"all": "Todas as mídias (Padrão)"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"companion_login": {
|
||||||
|
"title": "Pair with TV",
|
||||||
|
"align_qr": "Align the QR code within the frame",
|
||||||
|
"enter_code_manually": "Enter code manually",
|
||||||
|
"pairing_enter_credentials": "Enter credentials for TV",
|
||||||
|
"pairing_code_label": "Pairing code",
|
||||||
|
"server": "Server",
|
||||||
|
"authorize_button": "Authorize",
|
||||||
|
"authorizing": "Authorizing...",
|
||||||
|
"scan_again": "Scan Again",
|
||||||
|
"done": "Done",
|
||||||
|
"success_title": "Authorization Sent",
|
||||||
|
"pairing_tv_connecting": "The TV is connecting to your account",
|
||||||
|
"error_title": "Authorization Failed",
|
||||||
|
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
|
||||||
|
"error_generic": "Something went wrong. Please try again.",
|
||||||
|
"error_permission_denied": "Camera permission is required to scan QR codes.",
|
||||||
|
"login_as": "Log in as {{username}}?",
|
||||||
|
"on_server": "on {{server}}",
|
||||||
|
"use_different_user": "Use a different user",
|
||||||
|
"open_settings": "Open Settings"
|
||||||
|
},
|
||||||
|
"pairing": {
|
||||||
|
"pair_with_phone": "Pair with Phone",
|
||||||
|
"pair_with_phone_title": "Login TV",
|
||||||
|
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
|
||||||
|
"waiting_for_phone": "Waiting for phone...",
|
||||||
|
"scan_with_phone": "Scan with the Streamyfin app on your phone",
|
||||||
|
"logging_in": "Logging in...",
|
||||||
|
"logging_in_description": "Connecting to your server"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
"error_title": "Eroare",
|
"error_title": "Eroare",
|
||||||
"login_title": "Conectare",
|
"login_title": "Conectare",
|
||||||
"login_to_title": "Conectare la",
|
"login_to_title": "Conectare la",
|
||||||
|
"select_user": "Select a user to log in",
|
||||||
|
"add_user_to_login": "Add a user to log in",
|
||||||
|
"add_user": "Add User",
|
||||||
"username_placeholder": "Utilizator",
|
"username_placeholder": "Utilizator",
|
||||||
"password_placeholder": "Parola",
|
"password_placeholder": "Parola",
|
||||||
"login_button": "Conectare",
|
"login_button": "Conectare",
|
||||||
@@ -42,7 +45,13 @@
|
|||||||
"accounts_count": "{{count}} accounts",
|
"accounts_count": "{{count}} accounts",
|
||||||
"select_account": "Select Account",
|
"select_account": "Select Account",
|
||||||
"add_account": "Add Account",
|
"add_account": "Add Account",
|
||||||
"remove_account_description": "This will remove the saved credentials for {{username}}."
|
"remove_account_description": "This will remove the saved credentials for {{username}}.",
|
||||||
|
"remove_server": "Remove Server",
|
||||||
|
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
|
||||||
|
"select_your_server": "Select Your Server",
|
||||||
|
"add_server_to_get_started": "Add a server to get started",
|
||||||
|
"add_server": "Add Server",
|
||||||
|
"change_server": "Change Server"
|
||||||
},
|
},
|
||||||
"save_account": {
|
"save_account": {
|
||||||
"title": "Save Account",
|
"title": "Save Account",
|
||||||
@@ -86,6 +95,7 @@
|
|||||||
"oops": "Ups!",
|
"oops": "Ups!",
|
||||||
"error_message": "Ceva nu e bine.\nAutentificați-vă din nou.",
|
"error_message": "Ceva nu e bine.\nAutentificați-vă din nou.",
|
||||||
"continue_watching": "Continuă vizionarea",
|
"continue_watching": "Continuă vizionarea",
|
||||||
|
"continue": "Continue",
|
||||||
"next_up": "Urmează",
|
"next_up": "Urmează",
|
||||||
"continue_and_next_up": "Continue & Next Up",
|
"continue_and_next_up": "Continue & Next Up",
|
||||||
"recently_added_in": "Adăugat recent în {{libraryName}}",
|
"recently_added_in": "Adăugat recent în {{libraryName}}",
|
||||||
@@ -109,6 +119,12 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"settings_title": "Setări",
|
"settings_title": "Setări",
|
||||||
"log_out_button": "Deconectare",
|
"log_out_button": "Deconectare",
|
||||||
|
"switch_user": {
|
||||||
|
"title": "Switch User",
|
||||||
|
"account": "Account",
|
||||||
|
"switch_user": "Switch User on This Server",
|
||||||
|
"current": "current"
|
||||||
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "Categorii"
|
"title": "Categorii"
|
||||||
},
|
},
|
||||||
@@ -121,7 +137,16 @@
|
|||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "Aspect",
|
"title": "Aspect",
|
||||||
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
||||||
"hide_remote_session_button": "Hide Remote Session Button"
|
"hide_remote_session_button": "Hide Remote Session Button",
|
||||||
|
"show_home_backdrop": "Dynamic Home Backdrop",
|
||||||
|
"show_hero_carousel": "Hero Carousel",
|
||||||
|
"show_series_poster_on_episode": "Show Series Poster on Episodes",
|
||||||
|
"theme_music": "Theme Music",
|
||||||
|
"display_size": "Display Size",
|
||||||
|
"display_size_small": "Small",
|
||||||
|
"display_size_default": "Default",
|
||||||
|
"display_size_large": "Large",
|
||||||
|
"display_size_extra_large": "Extra Large"
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"title": "Network",
|
"title": "Network",
|
||||||
@@ -174,6 +199,22 @@
|
|||||||
"rewind_length": "Durata saltului înapoi",
|
"rewind_length": "Durata saltului înapoi",
|
||||||
"seconds_unit": "S"
|
"seconds_unit": "S"
|
||||||
},
|
},
|
||||||
|
"buffer": {
|
||||||
|
"title": "Buffer Settings",
|
||||||
|
"cache_mode": "Cache Mode",
|
||||||
|
"cache_auto": "Auto",
|
||||||
|
"cache_yes": "Enabled",
|
||||||
|
"cache_no": "Disabled",
|
||||||
|
"buffer_duration": "Buffer Duration",
|
||||||
|
"max_cache_size": "Max Cache Size",
|
||||||
|
"max_backward_cache": "Max Backward Cache"
|
||||||
|
},
|
||||||
|
"vo_driver": {
|
||||||
|
"title": "Video Output",
|
||||||
|
"vo_mode": "VO Driver",
|
||||||
|
"gpu_next": "gpu-next (Recommended)",
|
||||||
|
"gpu": "gpu"
|
||||||
|
},
|
||||||
"gesture_controls": {
|
"gesture_controls": {
|
||||||
"gesture_controls_title": "Controale gesturi",
|
"gesture_controls_title": "Controale gesturi",
|
||||||
"horizontal_swipe_skip": "Glisați orizontal pentru a sări",
|
"horizontal_swipe_skip": "Glisați orizontal pentru a sări",
|
||||||
@@ -256,7 +297,23 @@
|
|||||||
"subtitle_font": "Subtitle Font",
|
"subtitle_font": "Subtitle Font",
|
||||||
"ksplayer_title": "KSPlayer Settings",
|
"ksplayer_title": "KSPlayer Settings",
|
||||||
"hardware_decode": "Hardware Decoding",
|
"hardware_decode": "Hardware Decoding",
|
||||||
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
|
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.",
|
||||||
|
"opensubtitles_title": "OpenSubtitles",
|
||||||
|
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
|
||||||
|
"opensubtitles_api_key": "API Key",
|
||||||
|
"opensubtitles_api_key_placeholder": "Enter API key...",
|
||||||
|
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
|
||||||
|
"mpv_subtitle_scale": "Subtitle Scale",
|
||||||
|
"mpv_subtitle_margin_y": "Vertical Margin",
|
||||||
|
"mpv_subtitle_align_x": "Horizontal Align",
|
||||||
|
"mpv_subtitle_align_y": "Vertical Align",
|
||||||
|
"align": {
|
||||||
|
"left": "Left",
|
||||||
|
"center": "Center",
|
||||||
|
"right": "Right",
|
||||||
|
"top": "Top",
|
||||||
|
"bottom": "Bottom"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"vlc_subtitles": {
|
"vlc_subtitles": {
|
||||||
"title": "VLC Subtitle Settings",
|
"title": "VLC Subtitle Settings",
|
||||||
@@ -406,7 +463,13 @@
|
|||||||
"music_cache_cleared": "Music cache cleared",
|
"music_cache_cleared": "Music cache cleared",
|
||||||
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
||||||
"downloaded_songs_size": "{{size}} downloaded",
|
"downloaded_songs_size": "{{size}} downloaded",
|
||||||
"downloaded_songs_deleted": "Downloaded songs deleted"
|
"downloaded_songs_deleted": "Downloaded songs deleted",
|
||||||
|
"clear_all_cache": "Clear All Cache",
|
||||||
|
"clear_all_cache_confirm": "Clear All Cache?",
|
||||||
|
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
|
||||||
|
"clear_all_cache_success": "Cache Cleared",
|
||||||
|
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
|
||||||
|
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "Introducere",
|
"title": "Introducere",
|
||||||
@@ -430,6 +493,21 @@
|
|||||||
"error_deleting_files": "Eroare la ștergerea fișierelor",
|
"error_deleting_files": "Eroare la ștergerea fișierelor",
|
||||||
"background_downloads_enabled": "Descărcări în fundal activate",
|
"background_downloads_enabled": "Descărcări în fundal activate",
|
||||||
"background_downloads_disabled": "Descărcări în fundal dezactivate"
|
"background_downloads_disabled": "Descărcări în fundal dezactivate"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"title": "Security",
|
||||||
|
"inactivity_timeout": {
|
||||||
|
"title": "Inactivity Timeout",
|
||||||
|
"description": "Auto logout after inactivity",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"1_minute": "1 minute",
|
||||||
|
"5_minutes": "5 minutes",
|
||||||
|
"15_minutes": "15 minutes",
|
||||||
|
"30_minutes": "30 minutes",
|
||||||
|
"1_hour": "1 hour",
|
||||||
|
"4_hours": "4 hours",
|
||||||
|
"24_hours": "24 hours"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -456,6 +534,7 @@
|
|||||||
"new_app_version_requires_re_download_description": "Noua actualizare necesită descărcarea din nou a conținutului. Vă rugăm să eliminați tot conținutul descărcat și să încercați din nou.",
|
"new_app_version_requires_re_download_description": "Noua actualizare necesită descărcarea din nou a conținutului. Vă rugăm să eliminați tot conținutul descărcat și să încercați din nou.",
|
||||||
"back": "Înapoi",
|
"back": "Înapoi",
|
||||||
"delete": "Șterge",
|
"delete": "Șterge",
|
||||||
|
"delete_download": "Delete Download",
|
||||||
"something_went_wrong": "Ceva nu a mers bine.",
|
"something_went_wrong": "Ceva nu a mers bine.",
|
||||||
"could_not_get_stream_url_from_jellyfin": "Nu s-a putut obține adresa URL a fluxului de la Jellyfin",
|
"could_not_get_stream_url_from_jellyfin": "Nu s-a putut obține adresa URL a fluxului de la Jellyfin",
|
||||||
"eta": "Estimat {{eta}}",
|
"eta": "Estimat {{eta}}",
|
||||||
@@ -492,22 +571,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"no_results": "No Results",
|
||||||
"select": "Selectare",
|
"select": "Selectare",
|
||||||
"no_trailer_available": "Nicio remorcă disponibilă",
|
"no_trailer_available": "Nicio remorcă disponibilă",
|
||||||
"video": "Video",
|
"video": "Video",
|
||||||
"audio": "Audio",
|
"audio": "Audio",
|
||||||
"subtitle": "Subtitrare",
|
"subtitle": "Subtitrare",
|
||||||
"play": "Redare",
|
"play": "Redare",
|
||||||
|
"mark_as_played": "Mark as Played",
|
||||||
|
"mark_as_not_played": "Mark as not Played",
|
||||||
"none": "Nimic",
|
"none": "Nimic",
|
||||||
"track": "Limbă audio",
|
"track": "Limbă audio",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
|
"stop": "Stop",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"verifying": "Verifying..."
|
"verifying": "Verifying...",
|
||||||
|
"login": "Login",
|
||||||
|
"refresh": "Refresh"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"search": "Caută...",
|
"search": "Caută...",
|
||||||
@@ -556,6 +641,7 @@
|
|||||||
"movies": "filme",
|
"movies": "filme",
|
||||||
"series": "seriale",
|
"series": "seriale",
|
||||||
"boxsets": "box sets",
|
"boxsets": "box sets",
|
||||||
|
"playlists": "Playlists",
|
||||||
"items": "articole"
|
"items": "articole"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
@@ -566,7 +652,8 @@
|
|||||||
"poster": "Poster",
|
"poster": "Poster",
|
||||||
"cover": "Copertă",
|
"cover": "Copertă",
|
||||||
"show_titles": "Afișează titlurile",
|
"show_titles": "Afișează titlurile",
|
||||||
"show_stats": "Afișează statisticile"
|
"show_stats": "Afișează statisticile",
|
||||||
|
"options_title": "Options"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"genres": "Genuri",
|
"genres": "Genuri",
|
||||||
@@ -574,7 +661,11 @@
|
|||||||
"sort_by": "Sortează după",
|
"sort_by": "Sortează după",
|
||||||
"filter_by": "Filter By",
|
"filter_by": "Filter By",
|
||||||
"sort_order": "Ordine de sortare",
|
"sort_order": "Ordine de sortare",
|
||||||
"tags": "Taguri"
|
"tags": "Taguri",
|
||||||
|
"all": "All",
|
||||||
|
"reset": "Reset",
|
||||||
|
"asc": "Ascending",
|
||||||
|
"desc": "Descending"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"favorites": {
|
"favorites": {
|
||||||
@@ -591,6 +682,8 @@
|
|||||||
"no_links": "Niciun link"
|
"no_links": "Niciun link"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
|
"live": "LIVE",
|
||||||
|
"mpv_player_title": "MPV Player",
|
||||||
"error": "Eroare",
|
"error": "Eroare",
|
||||||
"failed_to_get_stream_url": "Nu s-a putut obține adresa URL a fluxului",
|
"failed_to_get_stream_url": "Nu s-a putut obține adresa URL a fluxului",
|
||||||
"an_error_occured_while_playing_the_video": "A apărut o eroare la redarea videoclipului. Verificați jurnalele în setări.",
|
"an_error_occured_while_playing_the_video": "A apărut o eroare la redarea videoclipului. Verificați jurnalele în setări.",
|
||||||
@@ -608,7 +701,35 @@
|
|||||||
"downloaded_file_message": "Doriți să redați fișierul descărcat?",
|
"downloaded_file_message": "Doriți să redați fișierul descărcat?",
|
||||||
"downloaded_file_yes": "Da",
|
"downloaded_file_yes": "Da",
|
||||||
"downloaded_file_no": "Nu",
|
"downloaded_file_no": "Nu",
|
||||||
"downloaded_file_cancel": "Anulează"
|
"downloaded_file_cancel": "Anulează",
|
||||||
|
"swipe_down_settings": "Swipe down for settings",
|
||||||
|
"ends_at": "Ends at {{time}}",
|
||||||
|
"search_subtitles": "Search Subtitles",
|
||||||
|
"subtitle_tracks": "Tracks",
|
||||||
|
"subtitle_search": "Search & Download",
|
||||||
|
"download": "Download",
|
||||||
|
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
|
||||||
|
"using_jellyfin_server": "Using Jellyfin Server",
|
||||||
|
"language": "Language",
|
||||||
|
"results": "Results",
|
||||||
|
"searching": "Searching...",
|
||||||
|
"search_failed": "Search failed",
|
||||||
|
"no_subtitle_provider": "No subtitle provider configured on server",
|
||||||
|
"no_subtitles_found": "No subtitles found",
|
||||||
|
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
|
||||||
|
"settings": "Settings",
|
||||||
|
"skip_intro": "Skip Intro",
|
||||||
|
"skip_credits": "Skip Credits",
|
||||||
|
"stopPlayback": "Stop Playback",
|
||||||
|
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
|
||||||
|
"stopPlayingConfirm": "Are you sure you want to stop playback?",
|
||||||
|
"downloaded": "Downloaded"
|
||||||
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Urmează",
|
"next_up": "Urmează",
|
||||||
@@ -617,6 +738,11 @@
|
|||||||
"series": "Seriale",
|
"series": "Seriale",
|
||||||
"seasons": "Sezoane",
|
"seasons": "Sezoane",
|
||||||
"season": "Sezon",
|
"season": "Sezon",
|
||||||
|
"from_this_series": "From This Series",
|
||||||
|
"more_from_this_season": "More from this Season",
|
||||||
|
"view_series": "View Series",
|
||||||
|
"view_season": "View Season",
|
||||||
|
"select_season": "Select Season",
|
||||||
"no_episodes_for_this_season": "Niciun episod pt acest sezon",
|
"no_episodes_for_this_season": "Niciun episod pt acest sezon",
|
||||||
"overview": "Prezentare generală",
|
"overview": "Prezentare generală",
|
||||||
"more_with": "Mai multe cu {{name}}",
|
"more_with": "Mai multe cu {{name}}",
|
||||||
@@ -627,10 +753,21 @@
|
|||||||
"media_options": "Opțiuni Media",
|
"media_options": "Opțiuni Media",
|
||||||
"quality": "Calitate",
|
"quality": "Calitate",
|
||||||
"audio": "Audio",
|
"audio": "Audio",
|
||||||
"subtitles": "Subtitrare",
|
"subtitles": {
|
||||||
|
"label": "Subtitle",
|
||||||
|
"none": "None",
|
||||||
|
"tracks": "Tracks"
|
||||||
|
},
|
||||||
"show_more": "Arată mai mult",
|
"show_more": "Arată mai mult",
|
||||||
"show_less": "Arată mai puțin",
|
"show_less": "Arată mai puțin",
|
||||||
|
"left": "left",
|
||||||
|
"more_info": "More Info",
|
||||||
|
"director": "Director",
|
||||||
|
"cast": "Cast",
|
||||||
|
"technical_details": "Technical Details",
|
||||||
"appeared_in": "Apare în",
|
"appeared_in": "Apare în",
|
||||||
|
"movies": "Movies",
|
||||||
|
"shows": "Shows",
|
||||||
"could_not_load_item": "Nu s-a putut încărca elementul",
|
"could_not_load_item": "Nu s-a putut încărca elementul",
|
||||||
"none": "Nimic",
|
"none": "Nimic",
|
||||||
"download": {
|
"download": {
|
||||||
@@ -641,7 +778,13 @@
|
|||||||
"download_x_item": "Descărcați {{item_count}} articole",
|
"download_x_item": "Descărcați {{item_count}} articole",
|
||||||
"download_unwatched_only": "Numai nevizionate",
|
"download_unwatched_only": "Numai nevizionate",
|
||||||
"download_button": "Descarcă"
|
"download_button": "Descarcă"
|
||||||
}
|
},
|
||||||
|
"mark_played": "Mark as Watched",
|
||||||
|
"mark_unplayed": "Mark as Unwatched",
|
||||||
|
"resume_playback": "Resume Playback",
|
||||||
|
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
|
||||||
|
"play_from_start": "Play from Start",
|
||||||
|
"continue_from": "Continue from {{time}}"
|
||||||
},
|
},
|
||||||
"live_tv": {
|
"live_tv": {
|
||||||
"next": "Următorul",
|
"next": "Următorul",
|
||||||
@@ -652,7 +795,18 @@
|
|||||||
"movies": "Filme",
|
"movies": "Filme",
|
||||||
"sports": "Sport",
|
"sports": "Sport",
|
||||||
"for_kids": "Pt copii",
|
"for_kids": "Pt copii",
|
||||||
"news": "Știri"
|
"news": "Știri",
|
||||||
|
"page_of": "Page {{current}} of {{total}}",
|
||||||
|
"no_programs": "No programs available",
|
||||||
|
"no_channels": "No channels available",
|
||||||
|
"tabs": {
|
||||||
|
"programs": "Programs",
|
||||||
|
"guide": "Guide",
|
||||||
|
"channels": "Channels",
|
||||||
|
"recordings": "Recordings",
|
||||||
|
"schedule": "Schedule",
|
||||||
|
"series": "Series"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"jellyseerr": {
|
"jellyseerr": {
|
||||||
"confirm": "Confirmă",
|
"confirm": "Confirmă",
|
||||||
@@ -697,6 +851,12 @@
|
|||||||
"decline": "Respinge",
|
"decline": "Respinge",
|
||||||
"requested_by": "Solicitat de {{user}}",
|
"requested_by": "Solicitat de {{user}}",
|
||||||
"unknown_user": "Utilizator necunoscut",
|
"unknown_user": "Utilizator necunoscut",
|
||||||
|
"select": "Select",
|
||||||
|
"request_all": "Request All",
|
||||||
|
"request_seasons": "Request Seasons",
|
||||||
|
"select_seasons": "Select Seasons",
|
||||||
|
"request_selected": "Request Selected",
|
||||||
|
"n_selected": "{{count}} selected",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"jellyseer_does_not_meet_requirements": "Serverul Jellyseerr nu îndeplinește cerințele minime de versiune! Vă rugăm să actualizați cel puțin la versiunea 2.0.0",
|
"jellyseer_does_not_meet_requirements": "Serverul Jellyseerr nu îndeplinește cerințele minime de versiune! Vă rugăm să actualizați cel puțin la versiunea 2.0.0",
|
||||||
"jellyseerr_test_failed": "Testul Jellyseerr a eșuat. Vă rugăm să încercați din nou.",
|
"jellyseerr_test_failed": "Testul Jellyseerr a eșuat. Vă rugăm să încercați din nou.",
|
||||||
@@ -716,7 +876,8 @@
|
|||||||
"search": "Caută",
|
"search": "Caută",
|
||||||
"library": "Bibiliotecă",
|
"library": "Bibiliotecă",
|
||||||
"custom_links": "Linkuri personalizate",
|
"custom_links": "Linkuri personalizate",
|
||||||
"favorites": "Favorite"
|
"favorites": "Favorite",
|
||||||
|
"settings": "Settings"
|
||||||
},
|
},
|
||||||
"music": {
|
"music": {
|
||||||
"title": "Music",
|
"title": "Music",
|
||||||
@@ -841,5 +1002,36 @@
|
|||||||
"show": "This show",
|
"show": "This show",
|
||||||
"all": "All media (default)"
|
"all": "All media (default)"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"companion_login": {
|
||||||
|
"title": "Pair with TV",
|
||||||
|
"align_qr": "Align the QR code within the frame",
|
||||||
|
"enter_code_manually": "Enter code manually",
|
||||||
|
"pairing_enter_credentials": "Enter credentials for TV",
|
||||||
|
"pairing_code_label": "Pairing code",
|
||||||
|
"server": "Server",
|
||||||
|
"authorize_button": "Authorize",
|
||||||
|
"authorizing": "Authorizing...",
|
||||||
|
"scan_again": "Scan Again",
|
||||||
|
"done": "Done",
|
||||||
|
"success_title": "Authorization Sent",
|
||||||
|
"pairing_tv_connecting": "The TV is connecting to your account",
|
||||||
|
"error_title": "Authorization Failed",
|
||||||
|
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
|
||||||
|
"error_generic": "Something went wrong. Please try again.",
|
||||||
|
"error_permission_denied": "Camera permission is required to scan QR codes.",
|
||||||
|
"login_as": "Log in as {{username}}?",
|
||||||
|
"on_server": "on {{server}}",
|
||||||
|
"use_different_user": "Use a different user",
|
||||||
|
"open_settings": "Open Settings"
|
||||||
|
},
|
||||||
|
"pairing": {
|
||||||
|
"pair_with_phone": "Pair with Phone",
|
||||||
|
"pair_with_phone_title": "Login TV",
|
||||||
|
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
|
||||||
|
"waiting_for_phone": "Waiting for phone...",
|
||||||
|
"scan_with_phone": "Scan with the Streamyfin app on your phone",
|
||||||
|
"logging_in": "Logging in...",
|
||||||
|
"logging_in_description": "Connecting to your server"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
"error_title": "Ошибка",
|
"error_title": "Ошибка",
|
||||||
"login_title": "Вход",
|
"login_title": "Вход",
|
||||||
"login_to_title": "Вход в",
|
"login_to_title": "Вход в",
|
||||||
|
"select_user": "Select a user to log in",
|
||||||
|
"add_user_to_login": "Add a user to log in",
|
||||||
|
"add_user": "Add User",
|
||||||
"username_placeholder": "Имя пользователя",
|
"username_placeholder": "Имя пользователя",
|
||||||
"password_placeholder": "Пароль",
|
"password_placeholder": "Пароль",
|
||||||
"login_button": "Войти",
|
"login_button": "Войти",
|
||||||
@@ -12,25 +15,25 @@
|
|||||||
"failed_to_initiate_quick_connect": "Не удалось инициировать быстрое подключение",
|
"failed_to_initiate_quick_connect": "Не удалось инициировать быстрое подключение",
|
||||||
"got_it": "Принято",
|
"got_it": "Принято",
|
||||||
"connection_failed": "Соединение не удалось",
|
"connection_failed": "Соединение не удалось",
|
||||||
"could_not_connect_to_server": "Не удалось подключиться к серверу. Пожалуйста проверьте URL и ваше интернет соединение.",
|
"could_not_connect_to_server": "Не удалось подключиться к серверу. Пожалуйста, проверьте URL и ваше интернет-соединение.",
|
||||||
"an_unexpected_error_occured": "Возникла непредвиденная ошибка",
|
"an_unexpected_error_occured": "Возникла непредвиденная ошибка",
|
||||||
"change_server": "Поменять сервер",
|
"change_server": "Поменять сервер",
|
||||||
"invalid_username_or_password": "Неправильное имя пользователя или пароль",
|
"invalid_username_or_password": "Неправильное имя пользователя или пароль",
|
||||||
"user_does_not_have_permission_to_log_in": "Пользователь не имеет прав на вход",
|
"user_does_not_have_permission_to_log_in": "Пользователь не имеет прав на вход",
|
||||||
"server_is_taking_too_long_to_respond_try_again_later": "Сервер долго не отвечает, попробуйте позже.",
|
"server_is_taking_too_long_to_respond_try_again_later": "Сервер долго не отвечает, попробуйте позже",
|
||||||
"server_received_too_many_requests_try_again_later": "Сервер получил слишком много запросов, попробуйте позже.",
|
"server_received_too_many_requests_try_again_later": "Сервер получил слишком много запросов, попробуйте позже.",
|
||||||
"there_is_a_server_error": "Возникла ошибка сервера",
|
"there_is_a_server_error": "Возникла ошибка сервера",
|
||||||
"an_unexpected_error_occured_did_you_enter_the_correct_url": "Возникла непредвиденная ошибка. Вы правильно ввели URL?",
|
"an_unexpected_error_occured_did_you_enter_the_correct_url": "Возникла непредвиденная ошибка. Вы правильно ввели URL?",
|
||||||
"too_old_server_text": "Неподдерживаемый сервер Jellyfin обнаружен",
|
"too_old_server_text": "Обнаружен неподдерживаемый сервер Jellyfin",
|
||||||
"too_old_server_description": "Пожалуйста, обновите Jellyfin до последней версии"
|
"too_old_server_description": "Пожалуйста, обновите Jellyfin до последней версии"
|
||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"enter_url_to_jellyfin_server": "Укажите URL на ваш Jellyfin сервер",
|
"enter_url_to_jellyfin_server": "Укажите URL на ваш Jellyfin сервер",
|
||||||
"server_url_placeholder": "http(s)://your-server.com",
|
"server_url_placeholder": "http(s)://your-server.com",
|
||||||
"connect_button": "Подключиться",
|
"connect_button": "Подключиться",
|
||||||
"previous_servers": "предыдущие серверы",
|
"previous_servers": "Предыдущие серверы",
|
||||||
"clear_button": "Очистить",
|
"clear_button": "Очистить",
|
||||||
"swipe_to_remove": "Swipe to remove",
|
"swipe_to_remove": "Смахните для удаления",
|
||||||
"search_for_local_servers": "Поиск локальных серверов",
|
"search_for_local_servers": "Поиск локальных серверов",
|
||||||
"searching": "Поиск...",
|
"searching": "Поиск...",
|
||||||
"servers": "Сервера",
|
"servers": "Сервера",
|
||||||
@@ -39,10 +42,16 @@
|
|||||||
"please_login_again": "Ваша сессия истекла. Пожалуйста, войдите снова.",
|
"please_login_again": "Ваша сессия истекла. Пожалуйста, войдите снова.",
|
||||||
"remove_saved_login": "Удалить сохраненный аккаунт",
|
"remove_saved_login": "Удалить сохраненный аккаунт",
|
||||||
"remove_saved_login_description": "Ваши сохранённые данные для входа от этого сервера будут удалены. Вам придётся ввести ваши логин и пароль ещё раз.",
|
"remove_saved_login_description": "Ваши сохранённые данные для входа от этого сервера будут удалены. Вам придётся ввести ваши логин и пароль ещё раз.",
|
||||||
"accounts_count": "{{count}} аккаунтов",
|
"accounts_count": "Аккаунтов: {{count}}",
|
||||||
"select_account": "Выбрать аккаунт",
|
"select_account": "Выбрать аккаунт",
|
||||||
"add_account": "Добавить аккаунт",
|
"add_account": "Добавить аккаунт",
|
||||||
"remove_account_description": "Данные для входа {{username}} будут удалены."
|
"remove_account_description": "Данные для входа {{username}} будут удалены.",
|
||||||
|
"remove_server": "Remove Server",
|
||||||
|
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
|
||||||
|
"select_your_server": "Select Your Server",
|
||||||
|
"add_server_to_get_started": "Add a server to get started",
|
||||||
|
"add_server": "Add Server",
|
||||||
|
"change_server": "Change Server"
|
||||||
},
|
},
|
||||||
"save_account": {
|
"save_account": {
|
||||||
"title": "Сохранить аккаунт",
|
"title": "Сохранить аккаунт",
|
||||||
@@ -58,14 +67,14 @@
|
|||||||
"cancel_button": "Отмена"
|
"cancel_button": "Отмена"
|
||||||
},
|
},
|
||||||
"pin": {
|
"pin": {
|
||||||
"enter_pin": "Введите PIN",
|
"enter_pin": "Введите PIN-код",
|
||||||
"enter_pin_for": "Введите PIN для {{username}}",
|
"enter_pin_for": "Введите PIN-код для {{username}}",
|
||||||
"enter_4_digits": "Введите 4 цифры",
|
"enter_4_digits": "Введите 4 цифры",
|
||||||
"invalid_pin": "Некорректный PIN",
|
"invalid_pin": "Некорректный PIN-код",
|
||||||
"setup_pin": "Установить PIN",
|
"setup_pin": "Установить PIN-код",
|
||||||
"confirm_pin": "Подтвердите PIN",
|
"confirm_pin": "Подтвердите PIN-код",
|
||||||
"pins_dont_match": "PIN-коды не совпадают",
|
"pins_dont_match": "PIN-коды не совпадают",
|
||||||
"forgot_pin": "Забыли PIN?",
|
"forgot_pin": "Забыли PIN-код?",
|
||||||
"forgot_pin_desc": "Ваши данные для входа будут удалены"
|
"forgot_pin_desc": "Ваши данные для входа будут удалены"
|
||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
@@ -84,8 +93,9 @@
|
|||||||
"server_unreachable": "Сервер недоступен",
|
"server_unreachable": "Сервер недоступен",
|
||||||
"server_unreachable_message": "Не удалось соединиться с сервером.\nПожалуйста, проверьте настройки сети.",
|
"server_unreachable_message": "Не удалось соединиться с сервером.\nПожалуйста, проверьте настройки сети.",
|
||||||
"oops": "Упс!",
|
"oops": "Упс!",
|
||||||
"error_message": "Что-то пошло не так.\nПожалуйста выйдите и зайдите снова.",
|
"error_message": "Что-то пошло не так.\nПожалуйста, выйдите и зайдите снова.",
|
||||||
"continue_watching": "Продолжить",
|
"continue_watching": "Продолжить",
|
||||||
|
"continue": "Continue",
|
||||||
"next_up": "Далее",
|
"next_up": "Далее",
|
||||||
"continue_and_next_up": "Продолжить и Далее",
|
"continue_and_next_up": "Продолжить и Далее",
|
||||||
"recently_added_in": "Недавно добавлено в {{libraryName}}",
|
"recently_added_in": "Недавно добавлено в {{libraryName}}",
|
||||||
@@ -93,13 +103,13 @@
|
|||||||
"suggested_episodes": "Предложенные серии",
|
"suggested_episodes": "Предложенные серии",
|
||||||
"intro": {
|
"intro": {
|
||||||
"welcome_to_streamyfin": "Добро пожаловать в Streamyfin",
|
"welcome_to_streamyfin": "Добро пожаловать в Streamyfin",
|
||||||
"a_free_and_open_source_client_for_jellyfin": "Бесплатный клиент для Jellyfin с открытым кодом",
|
"a_free_and_open_source_client_for_jellyfin": "Бесплатный клиент для Jellyfin с открытым кодом.",
|
||||||
"features_title": "Функции",
|
"features_title": "Функции",
|
||||||
"features_description": "Streamyfin имеет множество функций и интегрируется с широким спектром программ, которое вы можете найти в меню настроек:",
|
"features_description": "Streamyfin имеет множество функций и интегрируется с широким спектром программ, которое вы можете найти в меню настроек:",
|
||||||
"jellyseerr_feature_description": "Подключитесь к Jellyseerr и запрашивайте фильмы прямо в приложении.",
|
"jellyseerr_feature_description": "Подключитесь к Jellyseerr и запрашивайте фильмы прямо в приложении.",
|
||||||
"downloads_feature_title": "Загрузки",
|
"downloads_feature_title": "Загрузки",
|
||||||
"downloads_feature_description": "Скачивайте фильмы и сериалы для просмотра без интернета. Используйте стандартный способ или установите сервер оптимизации для загрузки файлов в фоновом режиме.",
|
"downloads_feature_description": "Скачивайте фильмы и сериалы для просмотра без интернета. Используйте стандартный способ или установите сервер оптимизации для загрузки файлов в фоновом режиме.",
|
||||||
"chromecast_feature_description": "Транслируйте фильмы и сериалы на ваши устройста с поддержкой Chromecast.",
|
"chromecast_feature_description": "Транслируйте фильмы и сериалы на ваши устройства с поддержкой Chromecast.",
|
||||||
"centralised_settings_plugin_title": "Плагин для централизованной настройки",
|
"centralised_settings_plugin_title": "Плагин для централизованной настройки",
|
||||||
"centralised_settings_plugin_description": "Настраивайте параметры из централизованного места на сервере Jellyfin. Все настройки клиента для всех пользователей будут синхронизированы автоматически.",
|
"centralised_settings_plugin_description": "Настраивайте параметры из централизованного места на сервере Jellyfin. Все настройки клиента для всех пользователей будут синхронизированы автоматически.",
|
||||||
"done_button": "Готово",
|
"done_button": "Готово",
|
||||||
@@ -109,6 +119,12 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"settings_title": "Настройки",
|
"settings_title": "Настройки",
|
||||||
"log_out_button": "Выйти",
|
"log_out_button": "Выйти",
|
||||||
|
"switch_user": {
|
||||||
|
"title": "Switch User",
|
||||||
|
"account": "Account",
|
||||||
|
"switch_user": "Switch User on This Server",
|
||||||
|
"current": "current"
|
||||||
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "Категории"
|
"title": "Категории"
|
||||||
},
|
},
|
||||||
@@ -121,7 +137,16 @@
|
|||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "Внешний вид",
|
"title": "Внешний вид",
|
||||||
"merge_next_up_continue_watching": "Объединить «Продолжить» и «Далее»",
|
"merge_next_up_continue_watching": "Объединить «Продолжить» и «Далее»",
|
||||||
"hide_remote_session_button": "Скрыть кнопку «Удалённый сеанс»"
|
"hide_remote_session_button": "Скрыть кнопку «Удалённый сеанс»",
|
||||||
|
"show_home_backdrop": "Dynamic Home Backdrop",
|
||||||
|
"show_hero_carousel": "Hero Carousel",
|
||||||
|
"show_series_poster_on_episode": "Show Series Poster on Episodes",
|
||||||
|
"theme_music": "Theme Music",
|
||||||
|
"display_size": "Display Size",
|
||||||
|
"display_size_small": "Small",
|
||||||
|
"display_size_default": "Default",
|
||||||
|
"display_size_large": "Large",
|
||||||
|
"display_size_extra_large": "Extra Large"
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"title": "Сеть",
|
"title": "Сеть",
|
||||||
@@ -129,7 +154,7 @@
|
|||||||
"auto_switch_enabled": "Переключаться дома автоматически",
|
"auto_switch_enabled": "Переключаться дома автоматически",
|
||||||
"auto_switch_description": "Автоматически переключаться на локальный URL при присоединении к домашней WiFi сети",
|
"auto_switch_description": "Автоматически переключаться на локальный URL при присоединении к домашней WiFi сети",
|
||||||
"local_url": "Локальный URL",
|
"local_url": "Локальный URL",
|
||||||
"local_url_hint": "Введите локальный URL вашего сервера (e.g., http://192.168.1.100:8096)",
|
"local_url_hint": "Введите локальный URL вашего сервера (например, http://192.168.1.100:8096)",
|
||||||
"local_url_placeholder": "http://192.168.1.100:8096",
|
"local_url_placeholder": "http://192.168.1.100:8096",
|
||||||
"home_wifi_networks": "Домашние WiFi сети",
|
"home_wifi_networks": "Домашние WiFi сети",
|
||||||
"add_current_network": "Добавить \"{{ssid}}\"",
|
"add_current_network": "Добавить \"{{ssid}}\"",
|
||||||
@@ -160,28 +185,44 @@
|
|||||||
},
|
},
|
||||||
"quick_connect": {
|
"quick_connect": {
|
||||||
"quick_connect_title": "Быстрое подключение",
|
"quick_connect_title": "Быстрое подключение",
|
||||||
"authorize_button": "Авторизировать через быстрое подключение",
|
"authorize_button": "Авторизовать через быстрое подключение",
|
||||||
"enter_the_quick_connect_code": "Введите код для быстрого подключения...",
|
"enter_the_quick_connect_code": "Введите код для быстрого подключения...",
|
||||||
"success": "Успех",
|
"success": "Успех",
|
||||||
"quick_connect_autorized": "Быстрое подключение авторизовано",
|
"quick_connect_autorized": "Быстрое подключение авторизовано",
|
||||||
"error": "Ошибка",
|
"error": "Ошибка",
|
||||||
"invalid_code": "Неверный код",
|
"invalid_code": "Неверный код",
|
||||||
"authorize": "Авторизировать"
|
"authorize": "Авторизовать"
|
||||||
},
|
},
|
||||||
"media_controls": {
|
"media_controls": {
|
||||||
"media_controls_title": "Медиа-контроль",
|
"media_controls_title": "Управление воспроизведением",
|
||||||
"forward_skip_length": "Шаг перемотки вперёд",
|
"forward_skip_length": "Шаг перемотки вперёд",
|
||||||
"rewind_length": "Шаг перемотки назад",
|
"rewind_length": "Шаг перемотки назад",
|
||||||
"seconds_unit": "c"
|
"seconds_unit": "c"
|
||||||
},
|
},
|
||||||
|
"buffer": {
|
||||||
|
"title": "Buffer Settings",
|
||||||
|
"cache_mode": "Cache Mode",
|
||||||
|
"cache_auto": "Auto",
|
||||||
|
"cache_yes": "Enabled",
|
||||||
|
"cache_no": "Disabled",
|
||||||
|
"buffer_duration": "Buffer Duration",
|
||||||
|
"max_cache_size": "Max Cache Size",
|
||||||
|
"max_backward_cache": "Max Backward Cache"
|
||||||
|
},
|
||||||
|
"vo_driver": {
|
||||||
|
"title": "Video Output",
|
||||||
|
"vo_mode": "VO Driver",
|
||||||
|
"gpu_next": "gpu-next (Recommended)",
|
||||||
|
"gpu": "gpu"
|
||||||
|
},
|
||||||
"gesture_controls": {
|
"gesture_controls": {
|
||||||
"gesture_controls_title": "Управление жестами",
|
"gesture_controls_title": "Управление жестами",
|
||||||
"horizontal_swipe_skip": "Горизонтальный свайп для перемотки",
|
"horizontal_swipe_skip": "Проведите влево/вправо для перемотки",
|
||||||
"horizontal_swipe_skip_description": "Проведите влево/вправо, когда элементы управления скрыты, чтобы пропустить",
|
"horizontal_swipe_skip_description": "Проведите влево/вправо, когда элементы управления скрыты, чтобы перемотать",
|
||||||
"left_side_brightness": "Управление яркостью левой стороны",
|
"left_side_brightness": "Управление яркостью слева",
|
||||||
"left_side_brightness_description": "Смахните вверх/вниз на левой стороне для настройки яркости",
|
"left_side_brightness_description": "Проведите вверх/вниз на левой стороне для настройки яркости",
|
||||||
"right_side_volume": "Управление громкостью справа",
|
"right_side_volume": "Управление громкостью справа",
|
||||||
"right_side_volume_description": "Свайп вверх/вниз с правой стороны для настройки громкости",
|
"right_side_volume_description": "Проведите вверх/вниз с правой стороны для настройки громкости",
|
||||||
"hide_volume_slider": "Скрыть индикатор громкости",
|
"hide_volume_slider": "Скрыть индикатор громкости",
|
||||||
"hide_volume_slider_description": "Скрывает индикатор громкости в плеере",
|
"hide_volume_slider_description": "Скрывает индикатор громкости в плеере",
|
||||||
"hide_brightness_slider": "Скрыть индикатор яркости",
|
"hide_brightness_slider": "Скрыть индикатор яркости",
|
||||||
@@ -205,7 +246,7 @@
|
|||||||
},
|
},
|
||||||
"subtitles": {
|
"subtitles": {
|
||||||
"subtitle_title": "Субтитры",
|
"subtitle_title": "Субтитры",
|
||||||
"subtitle_hint": "Настройки отображения субтитров",
|
"subtitle_hint": "Настройки отображения субтитров.",
|
||||||
"subtitle_language": "Язык субтитров",
|
"subtitle_language": "Язык субтитров",
|
||||||
"subtitle_mode": "Режим субтитров",
|
"subtitle_mode": "Режим субтитров",
|
||||||
"set_subtitle_track": "Устанавливать субтитры из предыдущего элемента",
|
"set_subtitle_track": "Устанавливать субтитры из предыдущего элемента",
|
||||||
@@ -256,7 +297,23 @@
|
|||||||
"subtitle_font": "Шрифт субтитров",
|
"subtitle_font": "Шрифт субтитров",
|
||||||
"ksplayer_title": "Настройки KSPlayer",
|
"ksplayer_title": "Настройки KSPlayer",
|
||||||
"hardware_decode": "Аппаратное декодирование",
|
"hardware_decode": "Аппаратное декодирование",
|
||||||
"hardware_decode_description": "Использовать аппаратное ускорение для декодирования видео. Выключите, если наблюдаете проблемы с воспроизведением."
|
"hardware_decode_description": "Использовать аппаратное ускорение для декодирования видео. Выключите, если наблюдаете проблемы с воспроизведением.",
|
||||||
|
"opensubtitles_title": "OpenSubtitles",
|
||||||
|
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
|
||||||
|
"opensubtitles_api_key": "API Key",
|
||||||
|
"opensubtitles_api_key_placeholder": "Enter API key...",
|
||||||
|
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
|
||||||
|
"mpv_subtitle_scale": "Subtitle Scale",
|
||||||
|
"mpv_subtitle_margin_y": "Vertical Margin",
|
||||||
|
"mpv_subtitle_align_x": "Horizontal Align",
|
||||||
|
"mpv_subtitle_align_y": "Vertical Align",
|
||||||
|
"align": {
|
||||||
|
"left": "Left",
|
||||||
|
"center": "Center",
|
||||||
|
"right": "Right",
|
||||||
|
"top": "Top",
|
||||||
|
"bottom": "Bottom"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"vlc_subtitles": {
|
"vlc_subtitles": {
|
||||||
"title": "Настройки субтитров в VLC",
|
"title": "Настройки субтитров в VLC",
|
||||||
@@ -271,9 +328,9 @@
|
|||||||
"margin": "Отступ снизу"
|
"margin": "Отступ снизу"
|
||||||
},
|
},
|
||||||
"video_player": {
|
"video_player": {
|
||||||
"title": "Видеоплеер",
|
"title": "Видео плеер",
|
||||||
"video_player": "Видеоплеер",
|
"video_player": "Видео плеер",
|
||||||
"video_player_description": "Выберите видеоплеер в iOS.",
|
"video_player_description": "Выберите видео плеер в iOS.",
|
||||||
"ksplayer": "KSPlayer",
|
"ksplayer": "KSPlayer",
|
||||||
"vlc": "VLC"
|
"vlc": "VLC"
|
||||||
},
|
},
|
||||||
@@ -294,12 +351,12 @@
|
|||||||
"UNKNOWN": "Неизвестное"
|
"UNKNOWN": "Неизвестное"
|
||||||
},
|
},
|
||||||
"safe_area_in_controls": "Безопасная зона в элементах управления",
|
"safe_area_in_controls": "Безопасная зона в элементах управления",
|
||||||
"video_player": "Видеоплеер",
|
"video_player": "Видео плеер",
|
||||||
"video_players": {
|
"video_players": {
|
||||||
"VLC_3": "VLC 3",
|
"VLC_3": "VLC 3",
|
||||||
"VLC_4": "VLC 4 (Экспериментальный + PiP)"
|
"VLC_4": "VLC 4 (Экспериментальный + PiP)"
|
||||||
},
|
},
|
||||||
"show_custom_menu_links": "Показать ссылки кастомного меню",
|
"show_custom_menu_links": "Показать ссылки пользовательского меню",
|
||||||
"show_large_home_carousel": "Показывать большую карусель (beta)",
|
"show_large_home_carousel": "Показывать большую карусель (beta)",
|
||||||
"hide_libraries": "Скрыть библиотеки",
|
"hide_libraries": "Скрыть библиотеки",
|
||||||
"select_liraries_you_want_to_hide": "Выберите Библиотеки, которое хотите спрятать из вкладки Библиотеки и домашней страницы.",
|
"select_liraries_you_want_to_hide": "Выберите Библиотеки, которое хотите спрятать из вкладки Библиотеки и домашней страницы.",
|
||||||
@@ -307,7 +364,7 @@
|
|||||||
"default_quality": "Качество по умолчанию",
|
"default_quality": "Качество по умолчанию",
|
||||||
"default_playback_speed": "Скорость воспроизведения по умолчанию",
|
"default_playback_speed": "Скорость воспроизведения по умолчанию",
|
||||||
"auto_play_next_episode": "Автоматически воспроизводить следующий эпизод",
|
"auto_play_next_episode": "Автоматически воспроизводить следующий эпизод",
|
||||||
"max_auto_play_episode_count": "Максимальное количество автовоспроизведения эпизодов",
|
"max_auto_play_episode_count": "Максимальное количество авто воспроизводимых эпизодов",
|
||||||
"disabled": "Отключено"
|
"disabled": "Отключено"
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@@ -319,9 +376,9 @@
|
|||||||
"playback_description": "Настройте воспроизведение музыки.",
|
"playback_description": "Настройте воспроизведение музыки.",
|
||||||
"prefer_downloaded": "Предпочитать скачанные песни",
|
"prefer_downloaded": "Предпочитать скачанные песни",
|
||||||
"caching_title": "Кеширование",
|
"caching_title": "Кеширование",
|
||||||
"caching_description": "Автоматически предкешировать следующие треки для стабильного воспроизведения.",
|
"caching_description": "Автоматически кешировать следующие треки для стабильного воспроизведения.",
|
||||||
"lookahead_enabled": "Включить предкеширование",
|
"lookahead_enabled": "Включить предкеширование",
|
||||||
"lookahead_count": "Сколько предкешировать",
|
"lookahead_count": "Сколько треков предкешировать",
|
||||||
"max_cache_size": "Максимальное число предкешированных треков"
|
"max_cache_size": "Максимальное число предкешированных треков"
|
||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
@@ -329,8 +386,8 @@
|
|||||||
"jellyseerr": {
|
"jellyseerr": {
|
||||||
"jellyseerr_warning": "Эта интеграция находится на ранней стадии. Ожидайте изменений.",
|
"jellyseerr_warning": "Эта интеграция находится на ранней стадии. Ожидайте изменений.",
|
||||||
"server_url": "URL сервера",
|
"server_url": "URL сервера",
|
||||||
"server_url_hint": "Пример: http(s)://your-host.url\n(Добавьте порт если необходимо)",
|
"server_url_hint": "Пример: http(s)://your-host.url\n(добавьте порт если необходимо)",
|
||||||
"server_url_placeholder": "Jellyseerr URL...",
|
"server_url_placeholder": "Seerr URL...",
|
||||||
"password": "Пароль",
|
"password": "Пароль",
|
||||||
"password_placeholder": "Введите пароль для пользователя Jellyfin {{username}}",
|
"password_placeholder": "Введите пароль для пользователя Jellyfin {{username}}",
|
||||||
"login_button": "Войти",
|
"login_button": "Войти",
|
||||||
@@ -349,7 +406,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"marlin_search": {
|
"marlin_search": {
|
||||||
"enable_marlin_search": "Включить Marlin Search ",
|
"enable_marlin_search": "Включить Marlin Search",
|
||||||
"url": "URL-адрес",
|
"url": "URL-адрес",
|
||||||
"server_url_placeholder": "http(s)://domain.org:port",
|
"server_url_placeholder": "http(s)://domain.org:port",
|
||||||
"marlin_search_hint": "Введите URL для Marlin сервера. URL должен включать http or https и опционально порт.",
|
"marlin_search_hint": "Введите URL для Marlin сервера. URL должен включать http or https и опционально порт.",
|
||||||
@@ -399,14 +456,20 @@
|
|||||||
"size_used": "{{used}} из {{total}} использовано",
|
"size_used": "{{used}} из {{total}} использовано",
|
||||||
"delete_all_downloaded_files": "Удалить все загруженные файлы",
|
"delete_all_downloaded_files": "Удалить все загруженные файлы",
|
||||||
"music_cache_title": "Кеш музыки",
|
"music_cache_title": "Кеш музыки",
|
||||||
"music_cache_description": "Автоматически прекешировать песни по мере прослушивания для плавного воспроизведения и поддержки отсутствия интернета",
|
"music_cache_description": "Автоматически кешировать песни по мере прослушивания для плавного воспроизведения и поддержки отсутствия интернета",
|
||||||
"enable_music_cache": "Кешировать музыку",
|
"enable_music_cache": "Кешировать музыку",
|
||||||
"clear_music_cache": "Очистить кеш музыки",
|
"clear_music_cache": "Очистить кеш музыки",
|
||||||
"music_cache_size": "{{size}} кешировано",
|
"music_cache_size": "Кешировано: {{size}}",
|
||||||
"music_cache_cleared": "Кеш музыки очищен",
|
"music_cache_cleared": "Кеш музыки очищен",
|
||||||
"delete_all_downloaded_songs": "Удалить все скачанные песни",
|
"delete_all_downloaded_songs": "Удалить все скачанные песни",
|
||||||
"downloaded_songs_size": "{{size}} скачано",
|
"downloaded_songs_size": "Скачано: {{size}}",
|
||||||
"downloaded_songs_deleted": "Скачанные песни удалены"
|
"downloaded_songs_deleted": "Скачанные песни удалены",
|
||||||
|
"clear_all_cache": "Clear All Cache",
|
||||||
|
"clear_all_cache_confirm": "Clear All Cache?",
|
||||||
|
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
|
||||||
|
"clear_all_cache_success": "Cache Cleared",
|
||||||
|
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
|
||||||
|
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "Вступление",
|
"title": "Вступление",
|
||||||
@@ -415,7 +478,7 @@
|
|||||||
},
|
},
|
||||||
"logs": {
|
"logs": {
|
||||||
"logs_title": "Логи",
|
"logs_title": "Логи",
|
||||||
"export_logs": "Экспорт журналов",
|
"export_logs": "Сохранить логи",
|
||||||
"click_for_more_info": "Нажмите для получения дополнительной информации",
|
"click_for_more_info": "Нажмите для получения дополнительной информации",
|
||||||
"level": "Уровень",
|
"level": "Уровень",
|
||||||
"no_logs_available": "Логи не доступны",
|
"no_logs_available": "Логи не доступны",
|
||||||
@@ -430,6 +493,21 @@
|
|||||||
"error_deleting_files": "Ошибка при удалении файлов",
|
"error_deleting_files": "Ошибка при удалении файлов",
|
||||||
"background_downloads_enabled": "Фоновая загрузка включена",
|
"background_downloads_enabled": "Фоновая загрузка включена",
|
||||||
"background_downloads_disabled": "Фоновая загрузка отключена"
|
"background_downloads_disabled": "Фоновая загрузка отключена"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"title": "Security",
|
||||||
|
"inactivity_timeout": {
|
||||||
|
"title": "Inactivity Timeout",
|
||||||
|
"description": "Auto logout after inactivity",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"1_minute": "1 minute",
|
||||||
|
"5_minutes": "5 minutes",
|
||||||
|
"15_minutes": "15 minutes",
|
||||||
|
"30_minutes": "30 minutes",
|
||||||
|
"1_hour": "1 hour",
|
||||||
|
"4_hours": "4 hours",
|
||||||
|
"24_hours": "24 hours"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -453,9 +531,10 @@
|
|||||||
"no_active_downloads": "Нет активных загрузок",
|
"no_active_downloads": "Нет активных загрузок",
|
||||||
"active_downloads": "Активные",
|
"active_downloads": "Активные",
|
||||||
"new_app_version_requires_re_download": "Новая версия приложения требует повторной загрузки",
|
"new_app_version_requires_re_download": "Новая версия приложения требует повторной загрузки",
|
||||||
"new_app_version_requires_re_download_description": "Новая версия приложения требует повторной загрузки. Пожалуйста удалите всё и попробуйте заново.",
|
"new_app_version_requires_re_download_description": "Новая версия приложения требует повторной загрузки контента. Пожалуйста, удалите весь скачанный контент и попробуйте заново.",
|
||||||
"back": "Назад",
|
"back": "Назад",
|
||||||
"delete": "Удалить",
|
"delete": "Удалить",
|
||||||
|
"delete_download": "Delete Download",
|
||||||
"something_went_wrong": "Что-то пошло не так",
|
"something_went_wrong": "Что-то пошло не так",
|
||||||
"could_not_get_stream_url_from_jellyfin": "Не удалось получить ссылку трансляции из Jellyfin",
|
"could_not_get_stream_url_from_jellyfin": "Не удалось получить ссылку трансляции из Jellyfin",
|
||||||
"eta": "Осталось {{eta}}",
|
"eta": "Осталось {{eta}}",
|
||||||
@@ -465,53 +544,59 @@
|
|||||||
"failed_to_delete_all_movies": "Возникла ошибка при удалении всех фильмов",
|
"failed_to_delete_all_movies": "Возникла ошибка при удалении всех фильмов",
|
||||||
"deleted_all_tvseries_successfully": "Все сериалы были успешно удалены!",
|
"deleted_all_tvseries_successfully": "Все сериалы были успешно удалены!",
|
||||||
"failed_to_delete_all_tvseries": "Возникла ошибка при удалении всех сериалов",
|
"failed_to_delete_all_tvseries": "Возникла ошибка при удалении всех сериалов",
|
||||||
"deleted_media_successfully": "Другие носители успешно удалены!",
|
"deleted_media_successfully": "Остальные медиафайлы успешно удалены!",
|
||||||
"failed_to_delete_media": "Не удалось удалить другой файл",
|
"failed_to_delete_media": "Не удалось удалить остальные медиафайлы",
|
||||||
"download_deleted": "Удалено",
|
"download_deleted": "Загруженный контент удалён",
|
||||||
"download_cancelled": "Загрузка отменена",
|
"download_cancelled": "Загрузка отменена",
|
||||||
"could_not_delete_download": "Не удалось удалить загрузку",
|
"could_not_delete_download": "Не удалось удалить загрузку",
|
||||||
"download_paused": "На паузе",
|
"download_paused": "На паузе",
|
||||||
"could_not_pause_download": "Не удалось приостановить загрузку",
|
"could_not_pause_download": "Не удалось приостановить загрузку",
|
||||||
"download_resumed": "Продолжено",
|
"download_resumed": "Продолжено",
|
||||||
"could_not_resume_download": "Не удалось продолжить загрузку",
|
"could_not_resume_download": "Не удалось возобновить загрузку",
|
||||||
"download_completed": "Завершено",
|
"download_completed": "Завершено",
|
||||||
"download_failed": "Не удалось загрузить",
|
"download_failed": "Не удалось загрузить",
|
||||||
"download_failed_for_item": "Загрузка {{item}} провалилась с ошибкой: {{error}}",
|
"download_failed_for_item": "Загрузка {{item}} провалилась с ошибкой: {{error}}",
|
||||||
"download_completed_for_item": "{{item}} успешно загружен",
|
"download_completed_for_item": "{{item}} успешно загружен",
|
||||||
"download_started_for_item": "Загрузка началась для {{item}}",
|
"download_started_for_item": "Загрузка {{item}} началась",
|
||||||
"failed_to_start_download": "Не удалось начать загрузку",
|
"failed_to_start_download": "Не удалось начать загрузку",
|
||||||
"item_already_downloading": "{{item}} уже загружается",
|
"item_already_downloading": "{{item}} уже загружается",
|
||||||
"all_files_deleted": "Все загрузки удалены",
|
"all_files_deleted": "Все загрузки удалены",
|
||||||
"files_deleted_by_type": "{{count}} {{type}} удалён(о)",
|
"files_deleted_by_type": "Удалено: {{count}} {{type}}",
|
||||||
"all_files_folders_and_jobs_deleted_successfully": "Все файлы, папки, и задачи были успешно удалены",
|
"all_files_folders_and_jobs_deleted_successfully": "Все файлы, папки, и задачи были успешно удалены",
|
||||||
"failed_to_clean_cache_directory": "Не удалось очистить директорию кэша",
|
"failed_to_clean_cache_directory": "Не удалось очистить директорию кэша",
|
||||||
"could_not_get_download_url_for_item": "Не удалось получить URL загрузки для {{itemName}}",
|
"could_not_get_download_url_for_item": "Не удалось получить URL для загрузки {{itemName}}",
|
||||||
"go_to_downloads": "В загрузки",
|
"go_to_downloads": "В загрузки",
|
||||||
"file_deleted": "{{item}} удалён"
|
"file_deleted": "Удалено: {{item}}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"no_results": "No Results",
|
||||||
"select": "Выбрать",
|
"select": "Выбрать",
|
||||||
"no_trailer_available": "Трейлер недоступен",
|
"no_trailer_available": "Трейлер недоступен",
|
||||||
"video": "Видео",
|
"video": "Видео",
|
||||||
"audio": "Звук",
|
"audio": "Звук",
|
||||||
"subtitle": "Субтитры",
|
"subtitle": "Субтитры",
|
||||||
"play": "Воспроизвести",
|
"play": "Воспроизвести",
|
||||||
|
"mark_as_played": "Mark as Played",
|
||||||
|
"mark_as_not_played": "Mark as not Played",
|
||||||
"none": "Отсутствует",
|
"none": "Отсутствует",
|
||||||
"track": "Трек",
|
"track": "Трек",
|
||||||
"cancel": "Отмена",
|
"cancel": "Отмена",
|
||||||
|
"stop": "Stop",
|
||||||
"delete": "Удалить",
|
"delete": "Удалить",
|
||||||
"ok": "ОК",
|
"ok": "ОК",
|
||||||
"remove": "Удалить",
|
"remove": "Удалить",
|
||||||
"next": "Вперед",
|
"next": "Вперед",
|
||||||
"back": "Назад",
|
"back": "Назад",
|
||||||
"continue": "Продолжить",
|
"continue": "Продолжить",
|
||||||
"verifying": "Проверка..."
|
"verifying": "Проверка...",
|
||||||
|
"login": "Login",
|
||||||
|
"refresh": "Refresh"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"search": "Поиск...",
|
"search": "Поиск...",
|
||||||
"x_items": "{{count}} элементов",
|
"x_items": "Элементов: {{count}}",
|
||||||
"library": "Библиотека",
|
"library": "Библиотека",
|
||||||
"discover": "Найти новое",
|
"discover": "Найти новое",
|
||||||
"no_results": "Ничего не найдено",
|
"no_results": "Ничего не найдено",
|
||||||
@@ -529,14 +614,14 @@
|
|||||||
"request_series": "Запросить сериалы",
|
"request_series": "Запросить сериалы",
|
||||||
"recently_added": "Недавно добавлено",
|
"recently_added": "Недавно добавлено",
|
||||||
"recent_requests": "Недавно запрошено",
|
"recent_requests": "Недавно запрошено",
|
||||||
"plex_watchlist": "Список просмотра с Plex",
|
"plex_watchlist": "Список просмотра Plex",
|
||||||
"trending": "В тренде",
|
"trending": "В тренде",
|
||||||
"popular_movies": "Популярные фильмы",
|
"popular_movies": "Популярные фильмы",
|
||||||
"movie_genres": "Популярные жанры",
|
"movie_genres": "Популярные жанры",
|
||||||
"upcoming_movies": "Предстоящие фильмы",
|
"upcoming_movies": "Предстоящие фильмы",
|
||||||
"studios": "Студии",
|
"studios": "Студии",
|
||||||
"popular_tv": "Популярные сериалы",
|
"popular_tv": "Популярные сериалы",
|
||||||
"tv_genres": "жанры сериалов",
|
"tv_genres": "Жанры сериалов",
|
||||||
"upcoming_tv": "Предстоящие сериалы",
|
"upcoming_tv": "Предстоящие сериалы",
|
||||||
"networks": "Сети",
|
"networks": "Сети",
|
||||||
"tmdb_movie_keyword": "TMDB Ключевые слова фильмов",
|
"tmdb_movie_keyword": "TMDB Ключевые слова фильмов",
|
||||||
@@ -556,7 +641,8 @@
|
|||||||
"movies": "Фильмы",
|
"movies": "Фильмы",
|
||||||
"series": "Сериалы",
|
"series": "Сериалы",
|
||||||
"boxsets": "Коллекции",
|
"boxsets": "Коллекции",
|
||||||
"items": "элементы"
|
"playlists": "Playlists",
|
||||||
|
"items": "Элементы"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"display": "Отображать",
|
"display": "Отображать",
|
||||||
@@ -565,8 +651,9 @@
|
|||||||
"image_style": "Стиль изображения",
|
"image_style": "Стиль изображения",
|
||||||
"poster": "Постер",
|
"poster": "Постер",
|
||||||
"cover": "Обложка",
|
"cover": "Обложка",
|
||||||
"show_titles": "Показывать загаловки",
|
"show_titles": "Показывать заголовки",
|
||||||
"show_stats": "Показывать статистику"
|
"show_stats": "Показывать статистику",
|
||||||
|
"options_title": "Options"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"genres": "Жанры",
|
"genres": "Жанры",
|
||||||
@@ -574,7 +661,11 @@
|
|||||||
"sort_by": "Сортировка",
|
"sort_by": "Сортировка",
|
||||||
"filter_by": "Фильтр",
|
"filter_by": "Фильтр",
|
||||||
"sort_order": "Порядок",
|
"sort_order": "Порядок",
|
||||||
"tags": "Тэги"
|
"tags": "Теги",
|
||||||
|
"all": "All",
|
||||||
|
"reset": "Reset",
|
||||||
|
"asc": "Ascending",
|
||||||
|
"desc": "Descending"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"favorites": {
|
"favorites": {
|
||||||
@@ -591,6 +682,8 @@
|
|||||||
"no_links": "Нет ссылок"
|
"no_links": "Нет ссылок"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
|
"live": "LIVE",
|
||||||
|
"mpv_player_title": "MPV Player",
|
||||||
"error": "Ошибка",
|
"error": "Ошибка",
|
||||||
"failed_to_get_stream_url": "Не удалось получить URL потока",
|
"failed_to_get_stream_url": "Не удалось получить URL потока",
|
||||||
"an_error_occured_while_playing_the_video": "Возникла Неожиданная ошибка во время воспроизведения. Проверьте логи в настройках.",
|
"an_error_occured_while_playing_the_video": "Возникла Неожиданная ошибка во время воспроизведения. Проверьте логи в настройках.",
|
||||||
@@ -608,7 +701,35 @@
|
|||||||
"downloaded_file_message": "Хотите воспроизвести скачанный файл?",
|
"downloaded_file_message": "Хотите воспроизвести скачанный файл?",
|
||||||
"downloaded_file_yes": "Да",
|
"downloaded_file_yes": "Да",
|
||||||
"downloaded_file_no": "Нет",
|
"downloaded_file_no": "Нет",
|
||||||
"downloaded_file_cancel": "Отмена"
|
"downloaded_file_cancel": "Отмена",
|
||||||
|
"swipe_down_settings": "Swipe down for settings",
|
||||||
|
"ends_at": "Ends at {{time}}",
|
||||||
|
"search_subtitles": "Search Subtitles",
|
||||||
|
"subtitle_tracks": "Tracks",
|
||||||
|
"subtitle_search": "Search & Download",
|
||||||
|
"download": "Download",
|
||||||
|
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
|
||||||
|
"using_jellyfin_server": "Using Jellyfin Server",
|
||||||
|
"language": "Language",
|
||||||
|
"results": "Results",
|
||||||
|
"searching": "Searching...",
|
||||||
|
"search_failed": "Search failed",
|
||||||
|
"no_subtitle_provider": "No subtitle provider configured on server",
|
||||||
|
"no_subtitles_found": "No subtitles found",
|
||||||
|
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
|
||||||
|
"settings": "Settings",
|
||||||
|
"skip_intro": "Skip Intro",
|
||||||
|
"skip_credits": "Skip Credits",
|
||||||
|
"stopPlayback": "Stop Playback",
|
||||||
|
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
|
||||||
|
"stopPlayingConfirm": "Are you sure you want to stop playback?",
|
||||||
|
"downloaded": "Downloaded"
|
||||||
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Далее",
|
"next_up": "Далее",
|
||||||
@@ -617,6 +738,11 @@
|
|||||||
"series": "Серии",
|
"series": "Серии",
|
||||||
"seasons": "Сезоны",
|
"seasons": "Сезоны",
|
||||||
"season": "Сезон",
|
"season": "Сезон",
|
||||||
|
"from_this_series": "From This Series",
|
||||||
|
"more_from_this_season": "More from this Season",
|
||||||
|
"view_series": "View Series",
|
||||||
|
"view_season": "View Season",
|
||||||
|
"select_season": "Select Season",
|
||||||
"no_episodes_for_this_season": "В этом сезоне нет серий",
|
"no_episodes_for_this_season": "В этом сезоне нет серий",
|
||||||
"overview": "Обзор",
|
"overview": "Обзор",
|
||||||
"more_with": "Больше с {{name}}",
|
"more_with": "Больше с {{name}}",
|
||||||
@@ -624,13 +750,24 @@
|
|||||||
"no_similar_items_found": "Похожие элементы не найдены",
|
"no_similar_items_found": "Похожие элементы не найдены",
|
||||||
"video": "Видео",
|
"video": "Видео",
|
||||||
"more_details": "Больше деталей",
|
"more_details": "Больше деталей",
|
||||||
"media_options": "Media Options",
|
"media_options": "Опции медиа",
|
||||||
"quality": "Качество",
|
"quality": "Качество",
|
||||||
"audio": "Звук",
|
"audio": "Звук",
|
||||||
"subtitles": "Субтитры",
|
"subtitles": {
|
||||||
|
"label": "Subtitle",
|
||||||
|
"none": "None",
|
||||||
|
"tracks": "Tracks"
|
||||||
|
},
|
||||||
"show_more": "Показать больше",
|
"show_more": "Показать больше",
|
||||||
"show_less": "Показать меньше",
|
"show_less": "Показать меньше",
|
||||||
|
"left": "left",
|
||||||
|
"more_info": "More Info",
|
||||||
|
"director": "Director",
|
||||||
|
"cast": "Cast",
|
||||||
|
"technical_details": "Technical Details",
|
||||||
"appeared_in": "Появлялся в",
|
"appeared_in": "Появлялся в",
|
||||||
|
"movies": "Movies",
|
||||||
|
"shows": "Shows",
|
||||||
"could_not_load_item": "Не удалось загрузить элемент",
|
"could_not_load_item": "Не удалось загрузить элемент",
|
||||||
"none": "Отсутствует",
|
"none": "Отсутствует",
|
||||||
"download": {
|
"download": {
|
||||||
@@ -641,7 +778,13 @@
|
|||||||
"download_x_item": "Загрузить {{item_count}} элементов",
|
"download_x_item": "Загрузить {{item_count}} элементов",
|
||||||
"download_unwatched_only": "Только непросмотренные",
|
"download_unwatched_only": "Только непросмотренные",
|
||||||
"download_button": "Загрузить"
|
"download_button": "Загрузить"
|
||||||
}
|
},
|
||||||
|
"mark_played": "Mark as Watched",
|
||||||
|
"mark_unplayed": "Mark as Unwatched",
|
||||||
|
"resume_playback": "Resume Playback",
|
||||||
|
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
|
||||||
|
"play_from_start": "Play from Start",
|
||||||
|
"continue_from": "Continue from {{time}}"
|
||||||
},
|
},
|
||||||
"live_tv": {
|
"live_tv": {
|
||||||
"next": "Далее",
|
"next": "Далее",
|
||||||
@@ -652,7 +795,18 @@
|
|||||||
"movies": "Фильмы",
|
"movies": "Фильмы",
|
||||||
"sports": "Спорт",
|
"sports": "Спорт",
|
||||||
"for_kids": "Для детей",
|
"for_kids": "Для детей",
|
||||||
"news": "Новости"
|
"news": "Новости",
|
||||||
|
"page_of": "Page {{current}} of {{total}}",
|
||||||
|
"no_programs": "No programs available",
|
||||||
|
"no_channels": "No channels available",
|
||||||
|
"tabs": {
|
||||||
|
"programs": "Programs",
|
||||||
|
"guide": "Guide",
|
||||||
|
"channels": "Channels",
|
||||||
|
"recordings": "Recordings",
|
||||||
|
"schedule": "Schedule",
|
||||||
|
"series": "Series"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"jellyseerr": {
|
"jellyseerr": {
|
||||||
"confirm": "Подтвердить",
|
"confirm": "Подтвердить",
|
||||||
@@ -685,26 +839,32 @@
|
|||||||
"currently_streaming_on": "Сейчас доступно на",
|
"currently_streaming_on": "Сейчас доступно на",
|
||||||
"advanced": "Продвинутое",
|
"advanced": "Продвинутое",
|
||||||
"request_as": "Запросить как",
|
"request_as": "Запросить как",
|
||||||
"tags": "Тэги",
|
"tags": "Теги",
|
||||||
"quality_profile": "Профиль качества",
|
"quality_profile": "Профиль качества",
|
||||||
"root_folder": "Корневая папка",
|
"root_folder": "Корневая папка",
|
||||||
"season_all": "Сезон (все)",
|
"season_all": "Сезон (все)",
|
||||||
"season_number": "Сезон {{season_number}}",
|
"season_number": "Сезон {{season_number}}",
|
||||||
"number_episodes": "{{episode_number}} серий",
|
"number_episodes": "Серий: {{episode_number}}",
|
||||||
"born": "Рожден",
|
"born": "Рожден",
|
||||||
"appearances": "Появления",
|
"appearances": "Появления",
|
||||||
"approve": "Одобрить",
|
"approve": "Одобрить",
|
||||||
"decline": "Отклонить",
|
"decline": "Отклонить",
|
||||||
"requested_by": "Запрошено {{user}}",
|
"requested_by": "Запрошено {{user}}",
|
||||||
"unknown_user": "Неизвестный пользователь",
|
"unknown_user": "Неизвестный пользователь",
|
||||||
|
"select": "Select",
|
||||||
|
"request_all": "Request All",
|
||||||
|
"request_seasons": "Request Seasons",
|
||||||
|
"select_seasons": "Select Seasons",
|
||||||
|
"request_selected": "Request Selected",
|
||||||
|
"n_selected": "{{count}} selected",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"jellyseer_does_not_meet_requirements": "Сервер Jellyseerr не соответствует минимальным требованиям версии! Пожалуйста, обновите до версии не ниже 2.0.0",
|
"jellyseer_does_not_meet_requirements": "Сервер Jellyseerr не соответствует минимальным требованиям версии! Пожалуйста, обновите до версии не ниже 2.0.0",
|
||||||
"jellyseerr_test_failed": "Тест Jellyseerr не пройден. Попробуйте еще раз.",
|
"jellyseerr_test_failed": "Тест Jellyseerr не пройден. Попробуйте еще раз.",
|
||||||
"failed_to_test_jellyseerr_server_url": "Не удалось проверить URL-адрес сервера jellyseerr",
|
"failed_to_test_jellyseerr_server_url": "Не удалось проверить URL-адрес сервера Seerr",
|
||||||
"issue_submitted": "Проблема отправлена!",
|
"issue_submitted": "Проблема отправлена!",
|
||||||
"requested_item": "Запрошено {{item}}!",
|
"requested_item": "Запрошено {{item}}!",
|
||||||
"you_dont_have_permission_to_request": "У вас нет разрешения на запрос!",
|
"you_dont_have_permission_to_request": "У вас нет разрешения на запрос!",
|
||||||
"something_went_wrong_requesting_media": "Что-то пошло не так при запросе медиафайлов!",
|
"something_went_wrong_requesting_media": "Что-то пошло не так при запросе медиа!",
|
||||||
"request_approved": "Запрос одобрен!",
|
"request_approved": "Запрос одобрен!",
|
||||||
"request_declined": "Запрос отклонён!",
|
"request_declined": "Запрос отклонён!",
|
||||||
"failed_to_approve_request": "Не удалось одобрить запрос",
|
"failed_to_approve_request": "Не удалось одобрить запрос",
|
||||||
@@ -716,7 +876,8 @@
|
|||||||
"search": "Поиск",
|
"search": "Поиск",
|
||||||
"library": "Библиотека",
|
"library": "Библиотека",
|
||||||
"custom_links": "Ссылки",
|
"custom_links": "Ссылки",
|
||||||
"favorites": "Избранное"
|
"favorites": "Избранное",
|
||||||
|
"settings": "Settings"
|
||||||
},
|
},
|
||||||
"music": {
|
"music": {
|
||||||
"title": "Музыка",
|
"title": "Музыка",
|
||||||
@@ -801,7 +962,7 @@
|
|||||||
"name_label": "Название",
|
"name_label": "Название",
|
||||||
"name_placeholder": "Введите название списка",
|
"name_placeholder": "Введите название списка",
|
||||||
"description_label": "Описание",
|
"description_label": "Описание",
|
||||||
"description_placeholder": "Введите описание (не обязательно)",
|
"description_placeholder": "Введите описание (необязательно)",
|
||||||
"is_public_label": "Публичный",
|
"is_public_label": "Публичный",
|
||||||
"is_public_description": "Разрешить остальным пользователям видеть этот список",
|
"is_public_description": "Разрешить остальным пользователям видеть этот список",
|
||||||
"allowed_type_label": "Тип контента",
|
"allowed_type_label": "Тип контента",
|
||||||
@@ -841,5 +1002,36 @@
|
|||||||
"show": "Ко всему сериалу",
|
"show": "Ко всему сериалу",
|
||||||
"all": "Ко всем файлам (по умолчанию)"
|
"all": "Ко всем файлам (по умолчанию)"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"companion_login": {
|
||||||
|
"title": "Pair with TV",
|
||||||
|
"align_qr": "Align the QR code within the frame",
|
||||||
|
"enter_code_manually": "Enter code manually",
|
||||||
|
"pairing_enter_credentials": "Enter credentials for TV",
|
||||||
|
"pairing_code_label": "Pairing code",
|
||||||
|
"server": "Server",
|
||||||
|
"authorize_button": "Authorize",
|
||||||
|
"authorizing": "Authorizing...",
|
||||||
|
"scan_again": "Scan Again",
|
||||||
|
"done": "Done",
|
||||||
|
"success_title": "Authorization Sent",
|
||||||
|
"pairing_tv_connecting": "The TV is connecting to your account",
|
||||||
|
"error_title": "Authorization Failed",
|
||||||
|
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
|
||||||
|
"error_generic": "Something went wrong. Please try again.",
|
||||||
|
"error_permission_denied": "Camera permission is required to scan QR codes.",
|
||||||
|
"login_as": "Log in as {{username}}?",
|
||||||
|
"on_server": "on {{server}}",
|
||||||
|
"use_different_user": "Use a different user",
|
||||||
|
"open_settings": "Open Settings"
|
||||||
|
},
|
||||||
|
"pairing": {
|
||||||
|
"pair_with_phone": "Pair with Phone",
|
||||||
|
"pair_with_phone_title": "Login TV",
|
||||||
|
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
|
||||||
|
"waiting_for_phone": "Waiting for phone...",
|
||||||
|
"scan_with_phone": "Scan with the Streamyfin app on your phone",
|
||||||
|
"logging_in": "Logging in...",
|
||||||
|
"logging_in_description": "Connecting to your server"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
"error_title": "Fel",
|
"error_title": "Fel",
|
||||||
"login_title": "Logga in",
|
"login_title": "Logga in",
|
||||||
"login_to_title": "Logga in till",
|
"login_to_title": "Logga in till",
|
||||||
|
"select_user": "Select a user to log in",
|
||||||
|
"add_user_to_login": "Add a user to log in",
|
||||||
|
"add_user": "Add User",
|
||||||
"username_placeholder": "Användarnamn",
|
"username_placeholder": "Användarnamn",
|
||||||
"password_placeholder": "Lösenord",
|
"password_placeholder": "Lösenord",
|
||||||
"login_button": "Logga in",
|
"login_button": "Logga in",
|
||||||
@@ -44,7 +47,11 @@
|
|||||||
"add_account": "Lägg till konto",
|
"add_account": "Lägg till konto",
|
||||||
"remove_account_description": "Detta kommer att ta bort de sparade uppgifterna för {{username}}.",
|
"remove_account_description": "Detta kommer att ta bort de sparade uppgifterna för {{username}}.",
|
||||||
"remove_server": "Ta bort server",
|
"remove_server": "Ta bort server",
|
||||||
"remove_server_description": "Detta kommer att ta bort {{server}} och alla sparade konton från din lista."
|
"remove_server_description": "Detta kommer att ta bort {{server}} och alla sparade konton från din lista.",
|
||||||
|
"select_your_server": "Select Your Server",
|
||||||
|
"add_server_to_get_started": "Add a server to get started",
|
||||||
|
"add_server": "Add Server",
|
||||||
|
"change_server": "Change Server"
|
||||||
},
|
},
|
||||||
"save_account": {
|
"save_account": {
|
||||||
"title": "Spara konto",
|
"title": "Spara konto",
|
||||||
@@ -112,6 +119,12 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"settings_title": "Inställningar",
|
"settings_title": "Inställningar",
|
||||||
"log_out_button": "Logga ut",
|
"log_out_button": "Logga ut",
|
||||||
|
"switch_user": {
|
||||||
|
"title": "Switch User",
|
||||||
|
"account": "Account",
|
||||||
|
"switch_user": "Switch User on This Server",
|
||||||
|
"current": "current"
|
||||||
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "Kategorier"
|
"title": "Kategorier"
|
||||||
},
|
},
|
||||||
@@ -128,12 +141,12 @@
|
|||||||
"show_home_backdrop": "Dynamisk hembakgrund",
|
"show_home_backdrop": "Dynamisk hembakgrund",
|
||||||
"show_hero_carousel": "Hjältekarusell",
|
"show_hero_carousel": "Hjältekarusell",
|
||||||
"show_series_poster_on_episode": "Visa serieaffisch på avsnitt",
|
"show_series_poster_on_episode": "Visa serieaffisch på avsnitt",
|
||||||
|
"theme_music": "Temamusik",
|
||||||
"display_size": "Visningsstorlek",
|
"display_size": "Visningsstorlek",
|
||||||
"display_size_small": "Liten",
|
"display_size_small": "Liten",
|
||||||
"display_size_default": "Standard",
|
"display_size_default": "Standard",
|
||||||
"display_size_large": "Stor",
|
"display_size_large": "Stor",
|
||||||
"display_size_extra_large": "Extra stor",
|
"display_size_extra_large": "Extra stor"
|
||||||
"theme_music": "Temamusik"
|
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"title": "Nätverk",
|
"title": "Nätverk",
|
||||||
@@ -196,6 +209,12 @@
|
|||||||
"max_cache_size": "Max cachestorlek",
|
"max_cache_size": "Max cachestorlek",
|
||||||
"max_backward_cache": "Max bakåtcache"
|
"max_backward_cache": "Max bakåtcache"
|
||||||
},
|
},
|
||||||
|
"vo_driver": {
|
||||||
|
"title": "Video Output",
|
||||||
|
"vo_mode": "VO Driver",
|
||||||
|
"gpu_next": "gpu-next (Recommended)",
|
||||||
|
"gpu": "gpu"
|
||||||
|
},
|
||||||
"gesture_controls": {
|
"gesture_controls": {
|
||||||
"gesture_controls_title": "Gestkontroller",
|
"gesture_controls_title": "Gestkontroller",
|
||||||
"horizontal_swipe_skip": "Horisontell Svepning för att Hoppa Fram/Bak",
|
"horizontal_swipe_skip": "Horisontell Svepning för att Hoppa Fram/Bak",
|
||||||
@@ -371,7 +390,7 @@
|
|||||||
"server_url_placeholder": "Seerr URL",
|
"server_url_placeholder": "Seerr URL",
|
||||||
"password": "Lösenord",
|
"password": "Lösenord",
|
||||||
"password_placeholder": "Ange lösenord för Jellyfin användare {{username}}",
|
"password_placeholder": "Ange lösenord för Jellyfin användare {{username}}",
|
||||||
"login_button": "Logga in",
|
"login_button": "Login",
|
||||||
"total_media_requests": "Totalt antal mediaförfrågningar",
|
"total_media_requests": "Totalt antal mediaförfrågningar",
|
||||||
"movie_quota_limit": "Gräns för filmkvot",
|
"movie_quota_limit": "Gräns för filmkvot",
|
||||||
"movie_quota_days": "Filmkvot Dagar",
|
"movie_quota_days": "Filmkvot Dagar",
|
||||||
@@ -444,7 +463,13 @@
|
|||||||
"music_cache_cleared": "Musikcache rensad",
|
"music_cache_cleared": "Musikcache rensad",
|
||||||
"delete_all_downloaded_songs": "Ta bort alla nerladdade filer",
|
"delete_all_downloaded_songs": "Ta bort alla nerladdade filer",
|
||||||
"downloaded_songs_size": "{{size}} nedladdad",
|
"downloaded_songs_size": "{{size}} nedladdad",
|
||||||
"downloaded_songs_deleted": "Nedladdade låtar raderade"
|
"downloaded_songs_deleted": "Nedladdade låtar raderade",
|
||||||
|
"clear_all_cache": "Clear All Cache",
|
||||||
|
"clear_all_cache_confirm": "Clear All Cache?",
|
||||||
|
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
|
||||||
|
"clear_all_cache_success": "Cache Cleared",
|
||||||
|
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
|
||||||
|
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "Introduktion",
|
"title": "Introduktion",
|
||||||
@@ -468,6 +493,21 @@
|
|||||||
"error_deleting_files": "Fel Vid Borttagning Av Filer",
|
"error_deleting_files": "Fel Vid Borttagning Av Filer",
|
||||||
"background_downloads_enabled": "Bakgrundsnedladdningar aktiverade",
|
"background_downloads_enabled": "Bakgrundsnedladdningar aktiverade",
|
||||||
"background_downloads_disabled": "Bakgrundsnedladdningar inaktiverade"
|
"background_downloads_disabled": "Bakgrundsnedladdningar inaktiverade"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"title": "Security",
|
||||||
|
"inactivity_timeout": {
|
||||||
|
"title": "Inactivity Timeout",
|
||||||
|
"description": "Auto logout after inactivity",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"1_minute": "1 minute",
|
||||||
|
"5_minutes": "5 minutes",
|
||||||
|
"15_minutes": "15 minutes",
|
||||||
|
"30_minutes": "30 minutes",
|
||||||
|
"1_hour": "1 hour",
|
||||||
|
"4_hours": "4 hours",
|
||||||
|
"24_hours": "24 hours"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -494,6 +534,7 @@
|
|||||||
"new_app_version_requires_re_download_description": "Den nya uppdateringen kräver att innehållet laddas ner igen. Ta bort allt nedladdat innehåll och försök igen.",
|
"new_app_version_requires_re_download_description": "Den nya uppdateringen kräver att innehållet laddas ner igen. Ta bort allt nedladdat innehåll och försök igen.",
|
||||||
"back": "Tillbaka",
|
"back": "Tillbaka",
|
||||||
"delete": "Radera",
|
"delete": "Radera",
|
||||||
|
"delete_download": "Delete Download",
|
||||||
"something_went_wrong": "Något Gick Fel",
|
"something_went_wrong": "Något Gick Fel",
|
||||||
"could_not_get_stream_url_from_jellyfin": "Det gick inte att hämta strömadressen från Jellyfin",
|
"could_not_get_stream_url_from_jellyfin": "Det gick inte att hämta strömadressen från Jellyfin",
|
||||||
"eta": "ETA {{eta}}",
|
"eta": "ETA {{eta}}",
|
||||||
@@ -537,6 +578,8 @@
|
|||||||
"audio": "Ljud",
|
"audio": "Ljud",
|
||||||
"subtitle": "Undertext",
|
"subtitle": "Undertext",
|
||||||
"play": "Spela",
|
"play": "Spela",
|
||||||
|
"mark_as_played": "Mark as Played",
|
||||||
|
"mark_as_not_played": "Mark as not Played",
|
||||||
"none": "Ingen",
|
"none": "Ingen",
|
||||||
"track": "Spår",
|
"track": "Spår",
|
||||||
"cancel": "Avbryt",
|
"cancel": "Avbryt",
|
||||||
@@ -549,8 +592,7 @@
|
|||||||
"continue": "Fortsätt",
|
"continue": "Fortsätt",
|
||||||
"verifying": "Verifierar...",
|
"verifying": "Verifierar...",
|
||||||
"login": "Logga in",
|
"login": "Logga in",
|
||||||
"refresh": "Uppdatera",
|
"refresh": "Uppdatera"
|
||||||
"seeAll": "Visa alla"
|
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"search": "Sök...",
|
"search": "Sök...",
|
||||||
@@ -610,7 +652,8 @@
|
|||||||
"poster": "Affisch",
|
"poster": "Affisch",
|
||||||
"cover": "Omslag",
|
"cover": "Omslag",
|
||||||
"show_titles": "Visa Titlar",
|
"show_titles": "Visa Titlar",
|
||||||
"show_stats": "Visa Statistik"
|
"show_stats": "Visa Statistik",
|
||||||
|
"options_title": "Options"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"genres": "Genrer",
|
"genres": "Genrer",
|
||||||
@@ -640,6 +683,7 @@
|
|||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
"live": "LIVE",
|
"live": "LIVE",
|
||||||
|
"mpv_player_title": "MPV Player",
|
||||||
"error": "Fel",
|
"error": "Fel",
|
||||||
"failed_to_get_stream_url": "Kunde inte hämta stream-URL",
|
"failed_to_get_stream_url": "Kunde inte hämta stream-URL",
|
||||||
"an_error_occured_while_playing_the_video": "Ett fel uppstod vid uppspelning av videon. Kontrollera loggarna i inställningarna.",
|
"an_error_occured_while_playing_the_video": "Ett fel uppstod vid uppspelning av videon. Kontrollera loggarna i inställningarna.",
|
||||||
@@ -681,6 +725,12 @@
|
|||||||
"stopPlayingConfirm": "Är du säker på att du vill stoppa uppspelningen?",
|
"stopPlayingConfirm": "Är du säker på att du vill stoppa uppspelningen?",
|
||||||
"downloaded": "Nedladdad"
|
"downloaded": "Nedladdad"
|
||||||
},
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Näst på tur",
|
"next_up": "Näst på tur",
|
||||||
"no_items_to_display": "Inga Artiklar Att Visa",
|
"no_items_to_display": "Inga Artiklar Att Visa",
|
||||||
@@ -793,7 +843,7 @@
|
|||||||
"quality_profile": "Kvalitetsprofil",
|
"quality_profile": "Kvalitetsprofil",
|
||||||
"root_folder": "Rotkatalog",
|
"root_folder": "Rotkatalog",
|
||||||
"season_all": "Säsong (alla)",
|
"season_all": "Säsong (alla)",
|
||||||
"season_number": "Säsong {{season_number}}",
|
"season_number": "Säsong {{seasonNumber}}",
|
||||||
"number_episodes": "{{episode_number}} Avsnitt",
|
"number_episodes": "{{episode_number}} Avsnitt",
|
||||||
"born": "Född",
|
"born": "Född",
|
||||||
"appearances": "Framträdanden",
|
"appearances": "Framträdanden",
|
||||||
@@ -952,5 +1002,36 @@
|
|||||||
"show": "Denna serie",
|
"show": "Denna serie",
|
||||||
"all": "Alla media (standard)"
|
"all": "Alla media (standard)"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"companion_login": {
|
||||||
|
"title": "Pair with TV",
|
||||||
|
"align_qr": "Align the QR code within the frame",
|
||||||
|
"enter_code_manually": "Enter code manually",
|
||||||
|
"pairing_enter_credentials": "Enter credentials for TV",
|
||||||
|
"pairing_code_label": "Pairing code",
|
||||||
|
"server": "Server",
|
||||||
|
"authorize_button": "Authorize",
|
||||||
|
"authorizing": "Authorizing...",
|
||||||
|
"scan_again": "Scan Again",
|
||||||
|
"done": "Done",
|
||||||
|
"success_title": "Authorization Sent",
|
||||||
|
"pairing_tv_connecting": "The TV is connecting to your account",
|
||||||
|
"error_title": "Authorization Failed",
|
||||||
|
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
|
||||||
|
"error_generic": "Something went wrong. Please try again.",
|
||||||
|
"error_permission_denied": "Camera permission is required to scan QR codes.",
|
||||||
|
"login_as": "Log in as {{username}}?",
|
||||||
|
"on_server": "on {{server}}",
|
||||||
|
"use_different_user": "Use a different user",
|
||||||
|
"open_settings": "Open Settings"
|
||||||
|
},
|
||||||
|
"pairing": {
|
||||||
|
"pair_with_phone": "Pair with Phone",
|
||||||
|
"pair_with_phone_title": "Login TV",
|
||||||
|
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
|
||||||
|
"waiting_for_phone": "Waiting for phone...",
|
||||||
|
"scan_with_phone": "Scan with the Streamyfin app on your phone",
|
||||||
|
"logging_in": "Logging in...",
|
||||||
|
"logging_in_description": "Connecting to your server"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
"error_title": "Error",
|
"error_title": "Error",
|
||||||
"login_title": "Log In",
|
"login_title": "Log In",
|
||||||
"login_to_title": "Log in to",
|
"login_to_title": "Log in to",
|
||||||
|
"select_user": "Select a user to log in",
|
||||||
|
"add_user_to_login": "Add a user to log in",
|
||||||
|
"add_user": "Add User",
|
||||||
"username_placeholder": "Username",
|
"username_placeholder": "Username",
|
||||||
"password_placeholder": "Password",
|
"password_placeholder": "Password",
|
||||||
"login_button": "Log In",
|
"login_button": "Log In",
|
||||||
@@ -42,7 +45,13 @@
|
|||||||
"accounts_count": "{{count}} accounts",
|
"accounts_count": "{{count}} accounts",
|
||||||
"select_account": "Select Account",
|
"select_account": "Select Account",
|
||||||
"add_account": "Add Account",
|
"add_account": "Add Account",
|
||||||
"remove_account_description": "This will remove the saved credentials for {{username}}."
|
"remove_account_description": "This will remove the saved credentials for {{username}}.",
|
||||||
|
"remove_server": "Remove Server",
|
||||||
|
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
|
||||||
|
"select_your_server": "Select Your Server",
|
||||||
|
"add_server_to_get_started": "Add a server to get started",
|
||||||
|
"add_server": "Add Server",
|
||||||
|
"change_server": "Change Server"
|
||||||
},
|
},
|
||||||
"save_account": {
|
"save_account": {
|
||||||
"title": "Save Account",
|
"title": "Save Account",
|
||||||
@@ -86,6 +95,7 @@
|
|||||||
"oops": "Oops!",
|
"oops": "Oops!",
|
||||||
"error_message": "Something went wrong.\nPlease log out and in again.",
|
"error_message": "Something went wrong.\nPlease log out and in again.",
|
||||||
"continue_watching": "Continue Watching",
|
"continue_watching": "Continue Watching",
|
||||||
|
"continue": "Continue",
|
||||||
"next_up": "Next Up",
|
"next_up": "Next Up",
|
||||||
"continue_and_next_up": "Continue & Next Up",
|
"continue_and_next_up": "Continue & Next Up",
|
||||||
"recently_added_in": "Recently Added in {{libraryName}}",
|
"recently_added_in": "Recently Added in {{libraryName}}",
|
||||||
@@ -109,6 +119,12 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"settings_title": "Settings",
|
"settings_title": "Settings",
|
||||||
"log_out_button": "Log Out",
|
"log_out_button": "Log Out",
|
||||||
|
"switch_user": {
|
||||||
|
"title": "Switch User",
|
||||||
|
"account": "Account",
|
||||||
|
"switch_user": "Switch User on This Server",
|
||||||
|
"current": "current"
|
||||||
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "Categories"
|
"title": "Categories"
|
||||||
},
|
},
|
||||||
@@ -121,7 +137,16 @@
|
|||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "ปรับแต่งลักษณะภายนอก",
|
"title": "ปรับแต่งลักษณะภายนอก",
|
||||||
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
||||||
"hide_remote_session_button": "Hide Remote Session Button"
|
"hide_remote_session_button": "Hide Remote Session Button",
|
||||||
|
"show_home_backdrop": "Dynamic Home Backdrop",
|
||||||
|
"show_hero_carousel": "Hero Carousel",
|
||||||
|
"show_series_poster_on_episode": "Show Series Poster on Episodes",
|
||||||
|
"theme_music": "Theme Music",
|
||||||
|
"display_size": "Display Size",
|
||||||
|
"display_size_small": "Small",
|
||||||
|
"display_size_default": "Default",
|
||||||
|
"display_size_large": "Large",
|
||||||
|
"display_size_extra_large": "Extra Large"
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"title": "Network",
|
"title": "Network",
|
||||||
@@ -174,6 +199,22 @@
|
|||||||
"rewind_length": "Rewind Length",
|
"rewind_length": "Rewind Length",
|
||||||
"seconds_unit": "s"
|
"seconds_unit": "s"
|
||||||
},
|
},
|
||||||
|
"buffer": {
|
||||||
|
"title": "Buffer Settings",
|
||||||
|
"cache_mode": "Cache Mode",
|
||||||
|
"cache_auto": "Auto",
|
||||||
|
"cache_yes": "Enabled",
|
||||||
|
"cache_no": "Disabled",
|
||||||
|
"buffer_duration": "Buffer Duration",
|
||||||
|
"max_cache_size": "Max Cache Size",
|
||||||
|
"max_backward_cache": "Max Backward Cache"
|
||||||
|
},
|
||||||
|
"vo_driver": {
|
||||||
|
"title": "Video Output",
|
||||||
|
"vo_mode": "VO Driver",
|
||||||
|
"gpu_next": "gpu-next (Recommended)",
|
||||||
|
"gpu": "gpu"
|
||||||
|
},
|
||||||
"gesture_controls": {
|
"gesture_controls": {
|
||||||
"gesture_controls_title": "Gesture Controls",
|
"gesture_controls_title": "Gesture Controls",
|
||||||
"horizontal_swipe_skip": "Horizontal Swipe to Skip",
|
"horizontal_swipe_skip": "Horizontal Swipe to Skip",
|
||||||
@@ -256,7 +297,23 @@
|
|||||||
"subtitle_font": "Subtitle Font",
|
"subtitle_font": "Subtitle Font",
|
||||||
"ksplayer_title": "KSPlayer Settings",
|
"ksplayer_title": "KSPlayer Settings",
|
||||||
"hardware_decode": "Hardware Decoding",
|
"hardware_decode": "Hardware Decoding",
|
||||||
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
|
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.",
|
||||||
|
"opensubtitles_title": "OpenSubtitles",
|
||||||
|
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
|
||||||
|
"opensubtitles_api_key": "API Key",
|
||||||
|
"opensubtitles_api_key_placeholder": "Enter API key...",
|
||||||
|
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
|
||||||
|
"mpv_subtitle_scale": "Subtitle Scale",
|
||||||
|
"mpv_subtitle_margin_y": "Vertical Margin",
|
||||||
|
"mpv_subtitle_align_x": "Horizontal Align",
|
||||||
|
"mpv_subtitle_align_y": "Vertical Align",
|
||||||
|
"align": {
|
||||||
|
"left": "Left",
|
||||||
|
"center": "Center",
|
||||||
|
"right": "Right",
|
||||||
|
"top": "Top",
|
||||||
|
"bottom": "Bottom"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"vlc_subtitles": {
|
"vlc_subtitles": {
|
||||||
"title": "VLC Subtitle Settings",
|
"title": "VLC Subtitle Settings",
|
||||||
@@ -406,7 +463,13 @@
|
|||||||
"music_cache_cleared": "Music cache cleared",
|
"music_cache_cleared": "Music cache cleared",
|
||||||
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
||||||
"downloaded_songs_size": "{{size}} downloaded",
|
"downloaded_songs_size": "{{size}} downloaded",
|
||||||
"downloaded_songs_deleted": "Downloaded songs deleted"
|
"downloaded_songs_deleted": "Downloaded songs deleted",
|
||||||
|
"clear_all_cache": "Clear All Cache",
|
||||||
|
"clear_all_cache_confirm": "Clear All Cache?",
|
||||||
|
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
|
||||||
|
"clear_all_cache_success": "Cache Cleared",
|
||||||
|
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
|
||||||
|
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "Intro",
|
"title": "Intro",
|
||||||
@@ -430,6 +493,21 @@
|
|||||||
"error_deleting_files": "Error Deleting Files",
|
"error_deleting_files": "Error Deleting Files",
|
||||||
"background_downloads_enabled": "Background downloads enabled",
|
"background_downloads_enabled": "Background downloads enabled",
|
||||||
"background_downloads_disabled": "Background downloads disabled"
|
"background_downloads_disabled": "Background downloads disabled"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"title": "Security",
|
||||||
|
"inactivity_timeout": {
|
||||||
|
"title": "Inactivity Timeout",
|
||||||
|
"description": "Auto logout after inactivity",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"1_minute": "1 minute",
|
||||||
|
"5_minutes": "5 minutes",
|
||||||
|
"15_minutes": "15 minutes",
|
||||||
|
"30_minutes": "30 minutes",
|
||||||
|
"1_hour": "1 hour",
|
||||||
|
"4_hours": "4 hours",
|
||||||
|
"24_hours": "24 hours"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -456,6 +534,7 @@
|
|||||||
"new_app_version_requires_re_download_description": "The new update requires content to be downloaded again. Please remove all downloaded content and try again.",
|
"new_app_version_requires_re_download_description": "The new update requires content to be downloaded again. Please remove all downloaded content and try again.",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
|
"delete_download": "Delete Download",
|
||||||
"something_went_wrong": "Something Went Wrong",
|
"something_went_wrong": "Something Went Wrong",
|
||||||
"could_not_get_stream_url_from_jellyfin": "Could not get the stream URL from Jellyfin",
|
"could_not_get_stream_url_from_jellyfin": "Could not get the stream URL from Jellyfin",
|
||||||
"eta": "ETA {{eta}}",
|
"eta": "ETA {{eta}}",
|
||||||
@@ -492,22 +571,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"no_results": "No Results",
|
||||||
"select": "Select",
|
"select": "Select",
|
||||||
"no_trailer_available": "No trailer available",
|
"no_trailer_available": "No trailer available",
|
||||||
"video": "Video",
|
"video": "Video",
|
||||||
"audio": "Audio",
|
"audio": "Audio",
|
||||||
"subtitle": "Subtitle",
|
"subtitle": "Subtitle",
|
||||||
"play": "Play",
|
"play": "Play",
|
||||||
|
"mark_as_played": "Mark as Played",
|
||||||
|
"mark_as_not_played": "Mark as not Played",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
"track": "Track",
|
"track": "Track",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
|
"stop": "Stop",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"verifying": "Verifying..."
|
"verifying": "Verifying...",
|
||||||
|
"login": "Login",
|
||||||
|
"refresh": "Refresh"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"search": "Search...",
|
"search": "Search...",
|
||||||
@@ -556,6 +641,7 @@
|
|||||||
"movies": "Movies",
|
"movies": "Movies",
|
||||||
"series": "Series",
|
"series": "Series",
|
||||||
"boxsets": "Box Sets",
|
"boxsets": "Box Sets",
|
||||||
|
"playlists": "Playlists",
|
||||||
"items": "Items"
|
"items": "Items"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
@@ -566,7 +652,8 @@
|
|||||||
"poster": "Poster",
|
"poster": "Poster",
|
||||||
"cover": "Cover",
|
"cover": "Cover",
|
||||||
"show_titles": "Show Titles",
|
"show_titles": "Show Titles",
|
||||||
"show_stats": "Show Stats"
|
"show_stats": "Show Stats",
|
||||||
|
"options_title": "Options"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"genres": "Genres",
|
"genres": "Genres",
|
||||||
@@ -574,7 +661,11 @@
|
|||||||
"sort_by": "Sort By",
|
"sort_by": "Sort By",
|
||||||
"filter_by": "Filter By",
|
"filter_by": "Filter By",
|
||||||
"sort_order": "Sort Order",
|
"sort_order": "Sort Order",
|
||||||
"tags": "Tags"
|
"tags": "Tags",
|
||||||
|
"all": "All",
|
||||||
|
"reset": "Reset",
|
||||||
|
"asc": "Ascending",
|
||||||
|
"desc": "Descending"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"favorites": {
|
"favorites": {
|
||||||
@@ -591,6 +682,8 @@
|
|||||||
"no_links": "No Links"
|
"no_links": "No Links"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
|
"live": "LIVE",
|
||||||
|
"mpv_player_title": "MPV Player",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"failed_to_get_stream_url": "Failed to get the stream URL",
|
"failed_to_get_stream_url": "Failed to get the stream URL",
|
||||||
"an_error_occured_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.",
|
"an_error_occured_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.",
|
||||||
@@ -608,7 +701,35 @@
|
|||||||
"downloaded_file_message": "Do you want to play the downloaded file?",
|
"downloaded_file_message": "Do you want to play the downloaded file?",
|
||||||
"downloaded_file_yes": "Yes",
|
"downloaded_file_yes": "Yes",
|
||||||
"downloaded_file_no": "No",
|
"downloaded_file_no": "No",
|
||||||
"downloaded_file_cancel": "Cancel"
|
"downloaded_file_cancel": "Cancel",
|
||||||
|
"swipe_down_settings": "Swipe down for settings",
|
||||||
|
"ends_at": "Ends at {{time}}",
|
||||||
|
"search_subtitles": "Search Subtitles",
|
||||||
|
"subtitle_tracks": "Tracks",
|
||||||
|
"subtitle_search": "Search & Download",
|
||||||
|
"download": "Download",
|
||||||
|
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
|
||||||
|
"using_jellyfin_server": "Using Jellyfin Server",
|
||||||
|
"language": "Language",
|
||||||
|
"results": "Results",
|
||||||
|
"searching": "Searching...",
|
||||||
|
"search_failed": "Search failed",
|
||||||
|
"no_subtitle_provider": "No subtitle provider configured on server",
|
||||||
|
"no_subtitles_found": "No subtitles found",
|
||||||
|
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
|
||||||
|
"settings": "Settings",
|
||||||
|
"skip_intro": "Skip Intro",
|
||||||
|
"skip_credits": "Skip Credits",
|
||||||
|
"stopPlayback": "Stop Playback",
|
||||||
|
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
|
||||||
|
"stopPlayingConfirm": "Are you sure you want to stop playback?",
|
||||||
|
"downloaded": "Downloaded"
|
||||||
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Next Up",
|
"next_up": "Next Up",
|
||||||
@@ -617,6 +738,11 @@
|
|||||||
"series": "Series",
|
"series": "Series",
|
||||||
"seasons": "Seasons",
|
"seasons": "Seasons",
|
||||||
"season": "Season",
|
"season": "Season",
|
||||||
|
"from_this_series": "From This Series",
|
||||||
|
"more_from_this_season": "More from this Season",
|
||||||
|
"view_series": "View Series",
|
||||||
|
"view_season": "View Season",
|
||||||
|
"select_season": "Select Season",
|
||||||
"no_episodes_for_this_season": "No episodes for this season",
|
"no_episodes_for_this_season": "No episodes for this season",
|
||||||
"overview": "Overview",
|
"overview": "Overview",
|
||||||
"more_with": "More with {{name}}",
|
"more_with": "More with {{name}}",
|
||||||
@@ -627,10 +753,21 @@
|
|||||||
"media_options": "Media Options",
|
"media_options": "Media Options",
|
||||||
"quality": "Quality",
|
"quality": "Quality",
|
||||||
"audio": "Audio",
|
"audio": "Audio",
|
||||||
"subtitles": "Subtitle",
|
"subtitles": {
|
||||||
|
"label": "Subtitle",
|
||||||
|
"none": "None",
|
||||||
|
"tracks": "Tracks"
|
||||||
|
},
|
||||||
"show_more": "Show More",
|
"show_more": "Show More",
|
||||||
"show_less": "Show Less",
|
"show_less": "Show Less",
|
||||||
|
"left": "left",
|
||||||
|
"more_info": "More Info",
|
||||||
|
"director": "Director",
|
||||||
|
"cast": "Cast",
|
||||||
|
"technical_details": "Technical Details",
|
||||||
"appeared_in": "Appeared In",
|
"appeared_in": "Appeared In",
|
||||||
|
"movies": "Movies",
|
||||||
|
"shows": "Shows",
|
||||||
"could_not_load_item": "Could Not Load Item",
|
"could_not_load_item": "Could Not Load Item",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
"download": {
|
"download": {
|
||||||
@@ -641,7 +778,13 @@
|
|||||||
"download_x_item": "Download {{item_count}} Items",
|
"download_x_item": "Download {{item_count}} Items",
|
||||||
"download_unwatched_only": "Unwatched Only",
|
"download_unwatched_only": "Unwatched Only",
|
||||||
"download_button": "Download"
|
"download_button": "Download"
|
||||||
}
|
},
|
||||||
|
"mark_played": "Mark as Watched",
|
||||||
|
"mark_unplayed": "Mark as Unwatched",
|
||||||
|
"resume_playback": "Resume Playback",
|
||||||
|
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
|
||||||
|
"play_from_start": "Play from Start",
|
||||||
|
"continue_from": "Continue from {{time}}"
|
||||||
},
|
},
|
||||||
"live_tv": {
|
"live_tv": {
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
@@ -652,7 +795,18 @@
|
|||||||
"movies": "Movies",
|
"movies": "Movies",
|
||||||
"sports": "Sports",
|
"sports": "Sports",
|
||||||
"for_kids": "For Kids",
|
"for_kids": "For Kids",
|
||||||
"news": "News"
|
"news": "News",
|
||||||
|
"page_of": "Page {{current}} of {{total}}",
|
||||||
|
"no_programs": "No programs available",
|
||||||
|
"no_channels": "No channels available",
|
||||||
|
"tabs": {
|
||||||
|
"programs": "Programs",
|
||||||
|
"guide": "Guide",
|
||||||
|
"channels": "Channels",
|
||||||
|
"recordings": "Recordings",
|
||||||
|
"schedule": "Schedule",
|
||||||
|
"series": "Series"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"jellyseerr": {
|
"jellyseerr": {
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
@@ -697,6 +851,12 @@
|
|||||||
"decline": "Decline",
|
"decline": "Decline",
|
||||||
"requested_by": "Requested by {{user}}",
|
"requested_by": "Requested by {{user}}",
|
||||||
"unknown_user": "Unknown User",
|
"unknown_user": "Unknown User",
|
||||||
|
"select": "Select",
|
||||||
|
"request_all": "Request All",
|
||||||
|
"request_seasons": "Request Seasons",
|
||||||
|
"select_seasons": "Select Seasons",
|
||||||
|
"request_selected": "Request Selected",
|
||||||
|
"n_selected": "{{count}} selected",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"jellyseer_does_not_meet_requirements": "Seerr server does not meet minimum version requirements! Please update to at least 2.0.0",
|
"jellyseer_does_not_meet_requirements": "Seerr server does not meet minimum version requirements! Please update to at least 2.0.0",
|
||||||
"jellyseerr_test_failed": "Seerr test failed. Please try again.",
|
"jellyseerr_test_failed": "Seerr test failed. Please try again.",
|
||||||
@@ -716,7 +876,8 @@
|
|||||||
"search": "Search",
|
"search": "Search",
|
||||||
"library": "Library",
|
"library": "Library",
|
||||||
"custom_links": "Custom Links",
|
"custom_links": "Custom Links",
|
||||||
"favorites": "Favorites"
|
"favorites": "Favorites",
|
||||||
|
"settings": "Settings"
|
||||||
},
|
},
|
||||||
"music": {
|
"music": {
|
||||||
"title": "Music",
|
"title": "Music",
|
||||||
@@ -841,5 +1002,36 @@
|
|||||||
"show": "This show",
|
"show": "This show",
|
||||||
"all": "All media (default)"
|
"all": "All media (default)"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"companion_login": {
|
||||||
|
"title": "Pair with TV",
|
||||||
|
"align_qr": "Align the QR code within the frame",
|
||||||
|
"enter_code_manually": "Enter code manually",
|
||||||
|
"pairing_enter_credentials": "Enter credentials for TV",
|
||||||
|
"pairing_code_label": "Pairing code",
|
||||||
|
"server": "Server",
|
||||||
|
"authorize_button": "Authorize",
|
||||||
|
"authorizing": "Authorizing...",
|
||||||
|
"scan_again": "Scan Again",
|
||||||
|
"done": "Done",
|
||||||
|
"success_title": "Authorization Sent",
|
||||||
|
"pairing_tv_connecting": "The TV is connecting to your account",
|
||||||
|
"error_title": "Authorization Failed",
|
||||||
|
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
|
||||||
|
"error_generic": "Something went wrong. Please try again.",
|
||||||
|
"error_permission_denied": "Camera permission is required to scan QR codes.",
|
||||||
|
"login_as": "Log in as {{username}}?",
|
||||||
|
"on_server": "on {{server}}",
|
||||||
|
"use_different_user": "Use a different user",
|
||||||
|
"open_settings": "Open Settings"
|
||||||
|
},
|
||||||
|
"pairing": {
|
||||||
|
"pair_with_phone": "Pair with Phone",
|
||||||
|
"pair_with_phone_title": "Login TV",
|
||||||
|
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
|
||||||
|
"waiting_for_phone": "Waiting for phone...",
|
||||||
|
"scan_with_phone": "Scan with the Streamyfin app on your phone",
|
||||||
|
"logging_in": "Logging in...",
|
||||||
|
"logging_in_description": "Connecting to your server"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
"error_title": "ghIq",
|
"error_title": "ghIq",
|
||||||
"login_title": "lut 'el",
|
"login_title": "lut 'el",
|
||||||
"login_to_title": "lut 'el",
|
"login_to_title": "lut 'el",
|
||||||
|
"select_user": "Select a user to log in",
|
||||||
|
"add_user_to_login": "Add a user to log in",
|
||||||
|
"add_user": "Add User",
|
||||||
"username_placeholder": "tlhIngan",
|
"username_placeholder": "tlhIngan",
|
||||||
"password_placeholder": "ngoq De'",
|
"password_placeholder": "ngoq De'",
|
||||||
"login_button": "yI'el!",
|
"login_button": "yI'el!",
|
||||||
@@ -42,7 +45,13 @@
|
|||||||
"accounts_count": "{{count}} accounts",
|
"accounts_count": "{{count}} accounts",
|
||||||
"select_account": "Select Account",
|
"select_account": "Select Account",
|
||||||
"add_account": "Add Account",
|
"add_account": "Add Account",
|
||||||
"remove_account_description": "This will remove the saved credentials for {{username}}."
|
"remove_account_description": "This will remove the saved credentials for {{username}}.",
|
||||||
|
"remove_server": "Remove Server",
|
||||||
|
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
|
||||||
|
"select_your_server": "Select Your Server",
|
||||||
|
"add_server_to_get_started": "Add a server to get started",
|
||||||
|
"add_server": "Add Server",
|
||||||
|
"change_server": "Change Server"
|
||||||
},
|
},
|
||||||
"save_account": {
|
"save_account": {
|
||||||
"title": "Save Account",
|
"title": "Save Account",
|
||||||
@@ -86,6 +95,7 @@
|
|||||||
"oops": "QI'ya!",
|
"oops": "QI'ya!",
|
||||||
"error_message": "Doch rurbe'.\nyIQo' 'ej yI'elqa'.",
|
"error_message": "Doch rurbe'.\nyIQo' 'ej yI'elqa'.",
|
||||||
"continue_watching": "tlhol yIHaDqa'",
|
"continue_watching": "tlhol yIHaDqa'",
|
||||||
|
"continue": "Continue",
|
||||||
"next_up": "wej",
|
"next_up": "wej",
|
||||||
"continue_and_next_up": "Continue & Next Up",
|
"continue_and_next_up": "Continue & Next Up",
|
||||||
"recently_added_in": "num tu'lu' {{libraryName}}",
|
"recently_added_in": "num tu'lu' {{libraryName}}",
|
||||||
@@ -109,6 +119,12 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"settings_title": "men",
|
"settings_title": "men",
|
||||||
"log_out_button": "yIQo'",
|
"log_out_button": "yIQo'",
|
||||||
|
"switch_user": {
|
||||||
|
"title": "Switch User",
|
||||||
|
"account": "Account",
|
||||||
|
"switch_user": "Switch User on This Server",
|
||||||
|
"current": "current"
|
||||||
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "Categories"
|
"title": "Categories"
|
||||||
},
|
},
|
||||||
@@ -121,7 +137,16 @@
|
|||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "Appearance",
|
"title": "Appearance",
|
||||||
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
"merge_next_up_continue_watching": "Merge Continue Watching & Next Up",
|
||||||
"hide_remote_session_button": "Hide Remote Session Button"
|
"hide_remote_session_button": "Hide Remote Session Button",
|
||||||
|
"show_home_backdrop": "Dynamic Home Backdrop",
|
||||||
|
"show_hero_carousel": "Hero Carousel",
|
||||||
|
"show_series_poster_on_episode": "Show Series Poster on Episodes",
|
||||||
|
"theme_music": "Theme Music",
|
||||||
|
"display_size": "Display Size",
|
||||||
|
"display_size_small": "Small",
|
||||||
|
"display_size_default": "Default",
|
||||||
|
"display_size_large": "Large",
|
||||||
|
"display_size_extra_large": "Extra Large"
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"title": "Network",
|
"title": "Network",
|
||||||
@@ -174,6 +199,22 @@
|
|||||||
"rewind_length": "bavHom vum",
|
"rewind_length": "bavHom vum",
|
||||||
"seconds_unit": "tera' rep"
|
"seconds_unit": "tera' rep"
|
||||||
},
|
},
|
||||||
|
"buffer": {
|
||||||
|
"title": "Buffer Settings",
|
||||||
|
"cache_mode": "Cache Mode",
|
||||||
|
"cache_auto": "Auto",
|
||||||
|
"cache_yes": "Enabled",
|
||||||
|
"cache_no": "Disabled",
|
||||||
|
"buffer_duration": "Buffer Duration",
|
||||||
|
"max_cache_size": "Max Cache Size",
|
||||||
|
"max_backward_cache": "Max Backward Cache"
|
||||||
|
},
|
||||||
|
"vo_driver": {
|
||||||
|
"title": "Video Output",
|
||||||
|
"vo_mode": "VO Driver",
|
||||||
|
"gpu_next": "gpu-next (Recommended)",
|
||||||
|
"gpu": "gpu"
|
||||||
|
},
|
||||||
"gesture_controls": {
|
"gesture_controls": {
|
||||||
"gesture_controls_title": "QavwI' 'ej Qap",
|
"gesture_controls_title": "QavwI' 'ej Qap",
|
||||||
"horizontal_swipe_skip": "SaS mup loSmeH",
|
"horizontal_swipe_skip": "SaS mup loSmeH",
|
||||||
@@ -256,7 +297,23 @@
|
|||||||
"subtitle_font": "Subtitle Font",
|
"subtitle_font": "Subtitle Font",
|
||||||
"ksplayer_title": "KSPlayer Settings",
|
"ksplayer_title": "KSPlayer Settings",
|
||||||
"hardware_decode": "Hardware Decoding",
|
"hardware_decode": "Hardware Decoding",
|
||||||
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues."
|
"hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues.",
|
||||||
|
"opensubtitles_title": "OpenSubtitles",
|
||||||
|
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
|
||||||
|
"opensubtitles_api_key": "API Key",
|
||||||
|
"opensubtitles_api_key_placeholder": "Enter API key...",
|
||||||
|
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
|
||||||
|
"mpv_subtitle_scale": "Subtitle Scale",
|
||||||
|
"mpv_subtitle_margin_y": "Vertical Margin",
|
||||||
|
"mpv_subtitle_align_x": "Horizontal Align",
|
||||||
|
"mpv_subtitle_align_y": "Vertical Align",
|
||||||
|
"align": {
|
||||||
|
"left": "Left",
|
||||||
|
"center": "Center",
|
||||||
|
"right": "Right",
|
||||||
|
"top": "Top",
|
||||||
|
"bottom": "Bottom"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"vlc_subtitles": {
|
"vlc_subtitles": {
|
||||||
"title": "VLC Subtitle Settings",
|
"title": "VLC Subtitle Settings",
|
||||||
@@ -406,7 +463,13 @@
|
|||||||
"music_cache_cleared": "Music cache cleared",
|
"music_cache_cleared": "Music cache cleared",
|
||||||
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
"delete_all_downloaded_songs": "Delete All Downloaded Songs",
|
||||||
"downloaded_songs_size": "{{size}} downloaded",
|
"downloaded_songs_size": "{{size}} downloaded",
|
||||||
"downloaded_songs_deleted": "Downloaded songs deleted"
|
"downloaded_songs_deleted": "Downloaded songs deleted",
|
||||||
|
"clear_all_cache": "Clear All Cache",
|
||||||
|
"clear_all_cache_confirm": "Clear All Cache?",
|
||||||
|
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
|
||||||
|
"clear_all_cache_success": "Cache Cleared",
|
||||||
|
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
|
||||||
|
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "Intro",
|
"title": "Intro",
|
||||||
@@ -430,6 +493,21 @@
|
|||||||
"error_deleting_files": "Qaw' ghIq",
|
"error_deleting_files": "Qaw' ghIq",
|
||||||
"background_downloads_enabled": "tlhegh Qaw' chu'",
|
"background_downloads_enabled": "tlhegh Qaw' chu'",
|
||||||
"background_downloads_disabled": "tlhegh Qaw' QIj"
|
"background_downloads_disabled": "tlhegh Qaw' QIj"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"title": "Security",
|
||||||
|
"inactivity_timeout": {
|
||||||
|
"title": "Inactivity Timeout",
|
||||||
|
"description": "Auto logout after inactivity",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"1_minute": "1 minute",
|
||||||
|
"5_minutes": "5 minutes",
|
||||||
|
"15_minutes": "15 minutes",
|
||||||
|
"30_minutes": "30 minutes",
|
||||||
|
"1_hour": "1 hour",
|
||||||
|
"4_hours": "4 hours",
|
||||||
|
"24_hours": "24 hours"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -456,6 +534,7 @@
|
|||||||
"new_app_version_requires_re_download_description": "wej chu' Doch Qaw'qa' DaneH. Hoch Qaw' Doch yIQaw' 'ej yIHaDqa'.",
|
"new_app_version_requires_re_download_description": "wej chu' Doch Qaw'qa' DaneH. Hoch Qaw' Doch yIQaw' 'ej yIHaDqa'.",
|
||||||
"back": "yIbav",
|
"back": "yIbav",
|
||||||
"delete": "yIQaw'",
|
"delete": "yIQaw'",
|
||||||
|
"delete_download": "Delete Download",
|
||||||
"something_went_wrong": "Doch rurbe'",
|
"something_went_wrong": "Doch rurbe'",
|
||||||
"could_not_get_stream_url_from_jellyfin": "Jellyfin tlhol ret URL tu'laHbe'",
|
"could_not_get_stream_url_from_jellyfin": "Jellyfin tlhol ret URL tu'laHbe'",
|
||||||
"eta": "ETA {{eta}}",
|
"eta": "ETA {{eta}}",
|
||||||
@@ -492,22 +571,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"no_results": "No Results",
|
||||||
"select": "Select",
|
"select": "Select",
|
||||||
"no_trailer_available": "No trailer available",
|
"no_trailer_available": "No trailer available",
|
||||||
"video": "mu'tlhegh",
|
"video": "mu'tlhegh",
|
||||||
"audio": "QoQ",
|
"audio": "QoQ",
|
||||||
"subtitle": "De' chu'",
|
"subtitle": "De' chu'",
|
||||||
"play": "Play",
|
"play": "Play",
|
||||||
|
"mark_as_played": "Mark as Played",
|
||||||
|
"mark_as_not_played": "Mark as not Played",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
"track": "Track",
|
"track": "Track",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
|
"stop": "Stop",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"verifying": "Verifying..."
|
"verifying": "Verifying...",
|
||||||
|
"login": "Login",
|
||||||
|
"refresh": "Refresh"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"search": "yISam...",
|
"search": "yISam...",
|
||||||
@@ -556,6 +641,7 @@
|
|||||||
"movies": "DIS",
|
"movies": "DIS",
|
||||||
"series": "Hem",
|
"series": "Hem",
|
||||||
"boxsets": "Hem ghom",
|
"boxsets": "Hem ghom",
|
||||||
|
"playlists": "Playlists",
|
||||||
"items": "Doch"
|
"items": "Doch"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
@@ -566,7 +652,8 @@
|
|||||||
"poster": "nagh",
|
"poster": "nagh",
|
||||||
"cover": "nagh chop",
|
"cover": "nagh chop",
|
||||||
"show_titles": "pab HoS yIHoch",
|
"show_titles": "pab HoS yIHoch",
|
||||||
"show_stats": "chIm De' yIHoch"
|
"show_stats": "chIm De' yIHoch",
|
||||||
|
"options_title": "Options"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"genres": "qorDu'",
|
"genres": "qorDu'",
|
||||||
@@ -574,7 +661,11 @@
|
|||||||
"sort_by": "yIwIv",
|
"sort_by": "yIwIv",
|
||||||
"filter_by": "Filter By",
|
"filter_by": "Filter By",
|
||||||
"sort_order": "wIv mIw",
|
"sort_order": "wIv mIw",
|
||||||
"tags": "De'Hom"
|
"tags": "De'Hom",
|
||||||
|
"all": "All",
|
||||||
|
"reset": "Reset",
|
||||||
|
"asc": "Ascending",
|
||||||
|
"desc": "Descending"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"favorites": {
|
"favorites": {
|
||||||
@@ -591,6 +682,8 @@
|
|||||||
"no_links": "ret pagh"
|
"no_links": "ret pagh"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
|
"live": "LIVE",
|
||||||
|
"mpv_player_title": "MPV Player",
|
||||||
"error": "ghIq",
|
"error": "ghIq",
|
||||||
"failed_to_get_stream_url": "tlhol ret URL tu'laHbe'",
|
"failed_to_get_stream_url": "tlhol ret URL tu'laHbe'",
|
||||||
"an_error_occured_while_playing_the_video": "mu'tlhegh tlholDI' ghIq. menDaq De' qon mej.",
|
"an_error_occured_while_playing_the_video": "mu'tlhegh tlholDI' ghIq. menDaq De' qon mej.",
|
||||||
@@ -608,7 +701,35 @@
|
|||||||
"downloaded_file_message": "Do you want to play the downloaded file?",
|
"downloaded_file_message": "Do you want to play the downloaded file?",
|
||||||
"downloaded_file_yes": "Yes",
|
"downloaded_file_yes": "Yes",
|
||||||
"downloaded_file_no": "No",
|
"downloaded_file_no": "No",
|
||||||
"downloaded_file_cancel": "Cancel"
|
"downloaded_file_cancel": "Cancel",
|
||||||
|
"swipe_down_settings": "Swipe down for settings",
|
||||||
|
"ends_at": "Ends at {{time}}",
|
||||||
|
"search_subtitles": "Search Subtitles",
|
||||||
|
"subtitle_tracks": "Tracks",
|
||||||
|
"subtitle_search": "Search & Download",
|
||||||
|
"download": "Download",
|
||||||
|
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
|
||||||
|
"using_jellyfin_server": "Using Jellyfin Server",
|
||||||
|
"language": "Language",
|
||||||
|
"results": "Results",
|
||||||
|
"searching": "Searching...",
|
||||||
|
"search_failed": "Search failed",
|
||||||
|
"no_subtitle_provider": "No subtitle provider configured on server",
|
||||||
|
"no_subtitles_found": "No subtitles found",
|
||||||
|
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
|
||||||
|
"settings": "Settings",
|
||||||
|
"skip_intro": "Skip Intro",
|
||||||
|
"skip_credits": "Skip Credits",
|
||||||
|
"stopPlayback": "Stop Playback",
|
||||||
|
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
|
||||||
|
"stopPlayingConfirm": "Are you sure you want to stop playback?",
|
||||||
|
"downloaded": "Downloaded"
|
||||||
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "wej",
|
"next_up": "wej",
|
||||||
@@ -617,6 +738,11 @@
|
|||||||
"series": "Hem",
|
"series": "Hem",
|
||||||
"seasons": "muv",
|
"seasons": "muv",
|
||||||
"season": "muv",
|
"season": "muv",
|
||||||
|
"from_this_series": "From This Series",
|
||||||
|
"more_from_this_season": "More from this Season",
|
||||||
|
"view_series": "View Series",
|
||||||
|
"view_season": "View Season",
|
||||||
|
"select_season": "Select Season",
|
||||||
"no_episodes_for_this_season": "muvvam HemHom pagh",
|
"no_episodes_for_this_season": "muvvam HemHom pagh",
|
||||||
"overview": "Hoch Sov",
|
"overview": "Hoch Sov",
|
||||||
"more_with": "{{name}} latlh",
|
"more_with": "{{name}} latlh",
|
||||||
@@ -627,10 +753,21 @@
|
|||||||
"media_options": "Media Options",
|
"media_options": "Media Options",
|
||||||
"quality": "luj",
|
"quality": "luj",
|
||||||
"audio": "QoQ",
|
"audio": "QoQ",
|
||||||
"subtitles": "De' chu'",
|
"subtitles": {
|
||||||
|
"label": "Subtitle",
|
||||||
|
"none": "None",
|
||||||
|
"tracks": "Tracks"
|
||||||
|
},
|
||||||
"show_more": "latlh yIHoch",
|
"show_more": "latlh yIHoch",
|
||||||
"show_less": "Hom yIHoch",
|
"show_less": "Hom yIHoch",
|
||||||
|
"left": "left",
|
||||||
|
"more_info": "More Info",
|
||||||
|
"director": "Director",
|
||||||
|
"cast": "Cast",
|
||||||
|
"technical_details": "Technical Details",
|
||||||
"appeared_in": "tlholvam",
|
"appeared_in": "tlholvam",
|
||||||
|
"movies": "Movies",
|
||||||
|
"shows": "Shows",
|
||||||
"could_not_load_item": "Doch tlha'laHbe'",
|
"could_not_load_item": "Doch tlha'laHbe'",
|
||||||
"none": "pagh",
|
"none": "pagh",
|
||||||
"download": {
|
"download": {
|
||||||
@@ -641,7 +778,13 @@
|
|||||||
"download_x_item": "{{item_count}} Doch yIQaw'",
|
"download_x_item": "{{item_count}} Doch yIQaw'",
|
||||||
"download_unwatched_only": "Unwatched Only",
|
"download_unwatched_only": "Unwatched Only",
|
||||||
"download_button": "yIQaw'"
|
"download_button": "yIQaw'"
|
||||||
}
|
},
|
||||||
|
"mark_played": "Mark as Watched",
|
||||||
|
"mark_unplayed": "Mark as Unwatched",
|
||||||
|
"resume_playback": "Resume Playback",
|
||||||
|
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
|
||||||
|
"play_from_start": "Play from Start",
|
||||||
|
"continue_from": "Continue from {{time}}"
|
||||||
},
|
},
|
||||||
"live_tv": {
|
"live_tv": {
|
||||||
"next": "wej",
|
"next": "wej",
|
||||||
@@ -652,7 +795,18 @@
|
|||||||
"movies": "DIS",
|
"movies": "DIS",
|
||||||
"sports": "QI'",
|
"sports": "QI'",
|
||||||
"for_kids": "puqbeq",
|
"for_kids": "puqbeq",
|
||||||
"news": "De'"
|
"news": "De'",
|
||||||
|
"page_of": "Page {{current}} of {{total}}",
|
||||||
|
"no_programs": "No programs available",
|
||||||
|
"no_channels": "No channels available",
|
||||||
|
"tabs": {
|
||||||
|
"programs": "Programs",
|
||||||
|
"guide": "Guide",
|
||||||
|
"channels": "Channels",
|
||||||
|
"recordings": "Recordings",
|
||||||
|
"schedule": "Schedule",
|
||||||
|
"series": "Series"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"jellyseerr": {
|
"jellyseerr": {
|
||||||
"confirm": "yInej",
|
"confirm": "yInej",
|
||||||
@@ -697,6 +851,12 @@
|
|||||||
"decline": "Decline",
|
"decline": "Decline",
|
||||||
"requested_by": "Requested by {{user}}",
|
"requested_by": "Requested by {{user}}",
|
||||||
"unknown_user": "Unknown User",
|
"unknown_user": "Unknown User",
|
||||||
|
"select": "Select",
|
||||||
|
"request_all": "Request All",
|
||||||
|
"request_seasons": "Request Seasons",
|
||||||
|
"select_seasons": "Select Seasons",
|
||||||
|
"request_selected": "Request Selected",
|
||||||
|
"n_selected": "{{count}} selected",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"jellyseer_does_not_meet_requirements": "Jellyseerr Ho'Do' veS wej law'be'! 2.0.0 yIchu'!",
|
"jellyseer_does_not_meet_requirements": "Jellyseerr Ho'Do' veS wej law'be'! 2.0.0 yIchu'!",
|
||||||
"jellyseerr_test_failed": "Jellyseerr nejlaHbe'. yIHaDqa'.",
|
"jellyseerr_test_failed": "Jellyseerr nejlaHbe'. yIHaDqa'.",
|
||||||
@@ -716,7 +876,8 @@
|
|||||||
"search": "Sam",
|
"search": "Sam",
|
||||||
"library": "De'wI' bom",
|
"library": "De'wI' bom",
|
||||||
"custom_links": "teqlu' ret",
|
"custom_links": "teqlu' ret",
|
||||||
"favorites": "wIv Doch"
|
"favorites": "wIv Doch",
|
||||||
|
"settings": "Settings"
|
||||||
},
|
},
|
||||||
"music": {
|
"music": {
|
||||||
"title": "Music",
|
"title": "Music",
|
||||||
@@ -841,5 +1002,36 @@
|
|||||||
"show": "This show",
|
"show": "This show",
|
||||||
"all": "All media (default)"
|
"all": "All media (default)"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"companion_login": {
|
||||||
|
"title": "Pair with TV",
|
||||||
|
"align_qr": "Align the QR code within the frame",
|
||||||
|
"enter_code_manually": "Enter code manually",
|
||||||
|
"pairing_enter_credentials": "Enter credentials for TV",
|
||||||
|
"pairing_code_label": "Pairing code",
|
||||||
|
"server": "Server",
|
||||||
|
"authorize_button": "Authorize",
|
||||||
|
"authorizing": "Authorizing...",
|
||||||
|
"scan_again": "Scan Again",
|
||||||
|
"done": "Done",
|
||||||
|
"success_title": "Authorization Sent",
|
||||||
|
"pairing_tv_connecting": "The TV is connecting to your account",
|
||||||
|
"error_title": "Authorization Failed",
|
||||||
|
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
|
||||||
|
"error_generic": "Something went wrong. Please try again.",
|
||||||
|
"error_permission_denied": "Camera permission is required to scan QR codes.",
|
||||||
|
"login_as": "Log in as {{username}}?",
|
||||||
|
"on_server": "on {{server}}",
|
||||||
|
"use_different_user": "Use a different user",
|
||||||
|
"open_settings": "Open Settings"
|
||||||
|
},
|
||||||
|
"pairing": {
|
||||||
|
"pair_with_phone": "Pair with Phone",
|
||||||
|
"pair_with_phone_title": "Login TV",
|
||||||
|
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
|
||||||
|
"waiting_for_phone": "Waiting for phone...",
|
||||||
|
"scan_with_phone": "Scan with the Streamyfin app on your phone",
|
||||||
|
"logging_in": "Logging in...",
|
||||||
|
"logging_in_description": "Connecting to your server"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
"error_title": "Hata",
|
"error_title": "Hata",
|
||||||
"login_title": "Giriş yap",
|
"login_title": "Giriş yap",
|
||||||
"login_to_title": " 'e giriş yap",
|
"login_to_title": " 'e giriş yap",
|
||||||
|
"select_user": "Select a user to log in",
|
||||||
|
"add_user_to_login": "Add a user to log in",
|
||||||
|
"add_user": "Add User",
|
||||||
"username_placeholder": "Kullanıcı adı",
|
"username_placeholder": "Kullanıcı adı",
|
||||||
"password_placeholder": "Şifre",
|
"password_placeholder": "Şifre",
|
||||||
"login_button": "Giriş yap",
|
"login_button": "Giriş yap",
|
||||||
@@ -42,7 +45,13 @@
|
|||||||
"accounts_count": "{{count}} hesap",
|
"accounts_count": "{{count}} hesap",
|
||||||
"select_account": "Hesap Seç",
|
"select_account": "Hesap Seç",
|
||||||
"add_account": "Hesap Ekle",
|
"add_account": "Hesap Ekle",
|
||||||
"remove_account_description": "{{username}} için kayıtlı bilgiler kaldırılacaktır."
|
"remove_account_description": "{{username}} için kayıtlı bilgiler kaldırılacaktır.",
|
||||||
|
"remove_server": "Remove Server",
|
||||||
|
"remove_server_description": "This will remove {{server}} and all saved accounts from your list.",
|
||||||
|
"select_your_server": "Select Your Server",
|
||||||
|
"add_server_to_get_started": "Add a server to get started",
|
||||||
|
"add_server": "Add Server",
|
||||||
|
"change_server": "Change Server"
|
||||||
},
|
},
|
||||||
"save_account": {
|
"save_account": {
|
||||||
"title": "Hesabı Kaydet",
|
"title": "Hesabı Kaydet",
|
||||||
@@ -86,6 +95,7 @@
|
|||||||
"oops": "Hups!",
|
"oops": "Hups!",
|
||||||
"error_message": "Bir şeyler ters gitti.\nLütfen çıkış yapıp tekrar giriş yapın.",
|
"error_message": "Bir şeyler ters gitti.\nLütfen çıkış yapıp tekrar giriş yapın.",
|
||||||
"continue_watching": "İzlemeye Devam Et",
|
"continue_watching": "İzlemeye Devam Et",
|
||||||
|
"continue": "Continue",
|
||||||
"next_up": "Sonraki",
|
"next_up": "Sonraki",
|
||||||
"continue_and_next_up": "İzlemeye Devam Et & Sıradakiler",
|
"continue_and_next_up": "İzlemeye Devam Et & Sıradakiler",
|
||||||
"recently_added_in": "{{libraryName}} Kütüphanesine Son Eklenenler",
|
"recently_added_in": "{{libraryName}} Kütüphanesine Son Eklenenler",
|
||||||
@@ -109,6 +119,12 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"settings_title": "Ayarlar",
|
"settings_title": "Ayarlar",
|
||||||
"log_out_button": "Çıkış Yap",
|
"log_out_button": "Çıkış Yap",
|
||||||
|
"switch_user": {
|
||||||
|
"title": "Switch User",
|
||||||
|
"account": "Account",
|
||||||
|
"switch_user": "Switch User on This Server",
|
||||||
|
"current": "current"
|
||||||
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "Kategoriler"
|
"title": "Kategoriler"
|
||||||
},
|
},
|
||||||
@@ -121,7 +137,16 @@
|
|||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "Görünüm",
|
"title": "Görünüm",
|
||||||
"merge_next_up_continue_watching": "İzlemeye Devam Et & Sıradakiler'i birleştir",
|
"merge_next_up_continue_watching": "İzlemeye Devam Et & Sıradakiler'i birleştir",
|
||||||
"hide_remote_session_button": "Uzak Oturum Butonunu Gizle"
|
"hide_remote_session_button": "Uzak Oturum Butonunu Gizle",
|
||||||
|
"show_home_backdrop": "Dynamic Home Backdrop",
|
||||||
|
"show_hero_carousel": "Hero Carousel",
|
||||||
|
"show_series_poster_on_episode": "Show Series Poster on Episodes",
|
||||||
|
"theme_music": "Theme Music",
|
||||||
|
"display_size": "Display Size",
|
||||||
|
"display_size_small": "Small",
|
||||||
|
"display_size_default": "Default",
|
||||||
|
"display_size_large": "Large",
|
||||||
|
"display_size_extra_large": "Extra Large"
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"title": "Ağ",
|
"title": "Ağ",
|
||||||
@@ -174,6 +199,22 @@
|
|||||||
"rewind_length": "Geri Sarma Uzunluğu",
|
"rewind_length": "Geri Sarma Uzunluğu",
|
||||||
"seconds_unit": "sn"
|
"seconds_unit": "sn"
|
||||||
},
|
},
|
||||||
|
"buffer": {
|
||||||
|
"title": "Buffer Settings",
|
||||||
|
"cache_mode": "Cache Mode",
|
||||||
|
"cache_auto": "Auto",
|
||||||
|
"cache_yes": "Enabled",
|
||||||
|
"cache_no": "Disabled",
|
||||||
|
"buffer_duration": "Buffer Duration",
|
||||||
|
"max_cache_size": "Max Cache Size",
|
||||||
|
"max_backward_cache": "Max Backward Cache"
|
||||||
|
},
|
||||||
|
"vo_driver": {
|
||||||
|
"title": "Video Output",
|
||||||
|
"vo_mode": "VO Driver",
|
||||||
|
"gpu_next": "gpu-next (Recommended)",
|
||||||
|
"gpu": "gpu"
|
||||||
|
},
|
||||||
"gesture_controls": {
|
"gesture_controls": {
|
||||||
"gesture_controls_title": "Hareketle Kontrol",
|
"gesture_controls_title": "Hareketle Kontrol",
|
||||||
"horizontal_swipe_skip": "Atlamak için yatay kaydırma",
|
"horizontal_swipe_skip": "Atlamak için yatay kaydırma",
|
||||||
@@ -256,7 +297,23 @@
|
|||||||
"subtitle_font": "Altyazı Yazı Tipi",
|
"subtitle_font": "Altyazı Yazı Tipi",
|
||||||
"ksplayer_title": "KSPlayer Ayarları",
|
"ksplayer_title": "KSPlayer Ayarları",
|
||||||
"hardware_decode": "Donanımsal Kod Çözme",
|
"hardware_decode": "Donanımsal Kod Çözme",
|
||||||
"hardware_decode_description": "Video kod çözme için donanımsal hızlandırma kullan. Oynatma sorunları yaşıyorsanız devre dışı bırakın."
|
"hardware_decode_description": "Video kod çözme için donanımsal hızlandırma kullan. Oynatma sorunları yaşıyorsanız devre dışı bırakın.",
|
||||||
|
"opensubtitles_title": "OpenSubtitles",
|
||||||
|
"opensubtitles_hint": "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured.",
|
||||||
|
"opensubtitles_api_key": "API Key",
|
||||||
|
"opensubtitles_api_key_placeholder": "Enter API key...",
|
||||||
|
"opensubtitles_get_key": "Get your free API key at opensubtitles.com/en/consumers",
|
||||||
|
"mpv_subtitle_scale": "Subtitle Scale",
|
||||||
|
"mpv_subtitle_margin_y": "Vertical Margin",
|
||||||
|
"mpv_subtitle_align_x": "Horizontal Align",
|
||||||
|
"mpv_subtitle_align_y": "Vertical Align",
|
||||||
|
"align": {
|
||||||
|
"left": "Left",
|
||||||
|
"center": "Center",
|
||||||
|
"right": "Right",
|
||||||
|
"top": "Top",
|
||||||
|
"bottom": "Bottom"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"vlc_subtitles": {
|
"vlc_subtitles": {
|
||||||
"title": "VLC Altyazı Ayarları",
|
"title": "VLC Altyazı Ayarları",
|
||||||
@@ -406,7 +463,13 @@
|
|||||||
"music_cache_cleared": "Müzik ön belleği temizlendi",
|
"music_cache_cleared": "Müzik ön belleği temizlendi",
|
||||||
"delete_all_downloaded_songs": "Tüm İndirilen Müzikleri Sil",
|
"delete_all_downloaded_songs": "Tüm İndirilen Müzikleri Sil",
|
||||||
"downloaded_songs_size": "{{size}} indirildi",
|
"downloaded_songs_size": "{{size}} indirildi",
|
||||||
"downloaded_songs_deleted": "İndirilen müzikler silindi"
|
"downloaded_songs_deleted": "İndirilen müzikler silindi",
|
||||||
|
"clear_all_cache": "Clear All Cache",
|
||||||
|
"clear_all_cache_confirm": "Clear All Cache?",
|
||||||
|
"clear_all_cache_confirm_desc": "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
|
||||||
|
"clear_all_cache_success": "Cache Cleared",
|
||||||
|
"clear_all_cache_success_desc": "All cache has been cleared successfully.",
|
||||||
|
"clear_all_cache_error_desc": "An error occurred while clearing the cache."
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "Giriş",
|
"title": "Giriş",
|
||||||
@@ -430,6 +493,21 @@
|
|||||||
"error_deleting_files": "Dosyalar silinirken hata oluştu",
|
"error_deleting_files": "Dosyalar silinirken hata oluştu",
|
||||||
"background_downloads_enabled": "Arka plan indirmeleri etkinleştirildi",
|
"background_downloads_enabled": "Arka plan indirmeleri etkinleştirildi",
|
||||||
"background_downloads_disabled": "Arka plan indirmeleri devre dışı bırakıldı"
|
"background_downloads_disabled": "Arka plan indirmeleri devre dışı bırakıldı"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"title": "Security",
|
||||||
|
"inactivity_timeout": {
|
||||||
|
"title": "Inactivity Timeout",
|
||||||
|
"description": "Auto logout after inactivity",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"1_minute": "1 minute",
|
||||||
|
"5_minutes": "5 minutes",
|
||||||
|
"15_minutes": "15 minutes",
|
||||||
|
"30_minutes": "30 minutes",
|
||||||
|
"1_hour": "1 hour",
|
||||||
|
"4_hours": "4 hours",
|
||||||
|
"24_hours": "24 hours"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -456,6 +534,7 @@
|
|||||||
"new_app_version_requires_re_download_description": "Yeni güncelleme, içeriğin yeniden indirilmesini gerektiriyor. Lütfen tüm indirilen içerikleri kaldırıp tekrar deneyin.",
|
"new_app_version_requires_re_download_description": "Yeni güncelleme, içeriğin yeniden indirilmesini gerektiriyor. Lütfen tüm indirilen içerikleri kaldırıp tekrar deneyin.",
|
||||||
"back": "Geri",
|
"back": "Geri",
|
||||||
"delete": "Sil",
|
"delete": "Sil",
|
||||||
|
"delete_download": "Delete Download",
|
||||||
"something_went_wrong": "Bir şeyler ters gitti",
|
"something_went_wrong": "Bir şeyler ters gitti",
|
||||||
"could_not_get_stream_url_from_jellyfin": "Jellyfin'den yayın URL'si alınamadı",
|
"could_not_get_stream_url_from_jellyfin": "Jellyfin'den yayın URL'si alınamadı",
|
||||||
"eta": "Tahmini Süre {{eta}}",
|
"eta": "Tahmini Süre {{eta}}",
|
||||||
@@ -492,22 +571,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"no_results": "No Results",
|
||||||
"select": "Seç",
|
"select": "Seç",
|
||||||
"no_trailer_available": "Fragman mevcut değil",
|
"no_trailer_available": "Fragman mevcut değil",
|
||||||
"video": "Video",
|
"video": "Video",
|
||||||
"audio": "Ses",
|
"audio": "Ses",
|
||||||
"subtitle": "Altyazı",
|
"subtitle": "Altyazı",
|
||||||
"play": "Oynat",
|
"play": "Oynat",
|
||||||
|
"mark_as_played": "Mark as Played",
|
||||||
|
"mark_as_not_played": "Mark as not Played",
|
||||||
"none": "Hiçbiri",
|
"none": "Hiçbiri",
|
||||||
"track": "Parça",
|
"track": "Parça",
|
||||||
"cancel": "Vazgeç",
|
"cancel": "Vazgeç",
|
||||||
|
"stop": "Stop",
|
||||||
"delete": "Sil",
|
"delete": "Sil",
|
||||||
"ok": "Tamam",
|
"ok": "Tamam",
|
||||||
"remove": "Kaldır",
|
"remove": "Kaldır",
|
||||||
"next": "Sonraki",
|
"next": "Sonraki",
|
||||||
"back": "Geri",
|
"back": "Geri",
|
||||||
"continue": "Devam",
|
"continue": "Devam",
|
||||||
"verifying": "Doğrulanıyor..."
|
"verifying": "Doğrulanıyor...",
|
||||||
|
"login": "Login",
|
||||||
|
"refresh": "Refresh"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"search": "Ara...",
|
"search": "Ara...",
|
||||||
@@ -556,6 +641,7 @@
|
|||||||
"movies": "filmler",
|
"movies": "filmler",
|
||||||
"series": "diziler",
|
"series": "diziler",
|
||||||
"boxsets": "koleksiyonlar",
|
"boxsets": "koleksiyonlar",
|
||||||
|
"playlists": "Playlists",
|
||||||
"items": "ögeler"
|
"items": "ögeler"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
@@ -566,7 +652,8 @@
|
|||||||
"poster": "Poster",
|
"poster": "Poster",
|
||||||
"cover": "Kapak",
|
"cover": "Kapak",
|
||||||
"show_titles": "Başlıkları göster",
|
"show_titles": "Başlıkları göster",
|
||||||
"show_stats": "İstatistikleri göster"
|
"show_stats": "İstatistikleri göster",
|
||||||
|
"options_title": "Options"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"genres": "Türler",
|
"genres": "Türler",
|
||||||
@@ -574,7 +661,11 @@
|
|||||||
"sort_by": "Sırala",
|
"sort_by": "Sırala",
|
||||||
"filter_by": "Filtrele",
|
"filter_by": "Filtrele",
|
||||||
"sort_order": "Sıralama düzeni",
|
"sort_order": "Sıralama düzeni",
|
||||||
"tags": "Etiketler"
|
"tags": "Etiketler",
|
||||||
|
"all": "All",
|
||||||
|
"reset": "Reset",
|
||||||
|
"asc": "Ascending",
|
||||||
|
"desc": "Descending"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"favorites": {
|
"favorites": {
|
||||||
@@ -591,6 +682,8 @@
|
|||||||
"no_links": "Bağlantı yok"
|
"no_links": "Bağlantı yok"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
|
"live": "LIVE",
|
||||||
|
"mpv_player_title": "MPV Player",
|
||||||
"error": "Hata",
|
"error": "Hata",
|
||||||
"failed_to_get_stream_url": "Yayın URL'si alınamadı",
|
"failed_to_get_stream_url": "Yayın URL'si alınamadı",
|
||||||
"an_error_occured_while_playing_the_video": "Video oynatılırken bir hata oluştu. Ayarlardaki günlüklere bakın.",
|
"an_error_occured_while_playing_the_video": "Video oynatılırken bir hata oluştu. Ayarlardaki günlüklere bakın.",
|
||||||
@@ -608,7 +701,35 @@
|
|||||||
"downloaded_file_message": "İndirilmiş dosyayı oynatmak ister misiniz?",
|
"downloaded_file_message": "İndirilmiş dosyayı oynatmak ister misiniz?",
|
||||||
"downloaded_file_yes": "Evet",
|
"downloaded_file_yes": "Evet",
|
||||||
"downloaded_file_no": "Hayır",
|
"downloaded_file_no": "Hayır",
|
||||||
"downloaded_file_cancel": "Vazgeç"
|
"downloaded_file_cancel": "Vazgeç",
|
||||||
|
"swipe_down_settings": "Swipe down for settings",
|
||||||
|
"ends_at": "Ends at {{time}}",
|
||||||
|
"search_subtitles": "Search Subtitles",
|
||||||
|
"subtitle_tracks": "Tracks",
|
||||||
|
"subtitle_search": "Search & Download",
|
||||||
|
"download": "Download",
|
||||||
|
"subtitle_download_hint": "Downloaded subtitles will be saved to your library",
|
||||||
|
"using_jellyfin_server": "Using Jellyfin Server",
|
||||||
|
"language": "Language",
|
||||||
|
"results": "Results",
|
||||||
|
"searching": "Searching...",
|
||||||
|
"search_failed": "Search failed",
|
||||||
|
"no_subtitle_provider": "No subtitle provider configured on server",
|
||||||
|
"no_subtitles_found": "No subtitles found",
|
||||||
|
"add_opensubtitles_key_hint": "Add OpenSubtitles API key in settings for client-side fallback",
|
||||||
|
"settings": "Settings",
|
||||||
|
"skip_intro": "Skip Intro",
|
||||||
|
"skip_credits": "Skip Credits",
|
||||||
|
"stopPlayback": "Stop Playback",
|
||||||
|
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
|
||||||
|
"stopPlayingConfirm": "Are you sure you want to stop playback?",
|
||||||
|
"downloaded": "Downloaded"
|
||||||
|
},
|
||||||
|
"chapters": {
|
||||||
|
"title": "Chapters",
|
||||||
|
"chapter_number": "Chapter {{number}}",
|
||||||
|
"open": "Open chapters",
|
||||||
|
"close": "Close chapters"
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Sıradaki",
|
"next_up": "Sıradaki",
|
||||||
@@ -617,6 +738,11 @@
|
|||||||
"series": "Dizi",
|
"series": "Dizi",
|
||||||
"seasons": "Sezonlar",
|
"seasons": "Sezonlar",
|
||||||
"season": "Sezon",
|
"season": "Sezon",
|
||||||
|
"from_this_series": "From This Series",
|
||||||
|
"more_from_this_season": "More from this Season",
|
||||||
|
"view_series": "View Series",
|
||||||
|
"view_season": "View Season",
|
||||||
|
"select_season": "Select Season",
|
||||||
"no_episodes_for_this_season": "Bu sezona ait bölüm yok",
|
"no_episodes_for_this_season": "Bu sezona ait bölüm yok",
|
||||||
"overview": "Özet",
|
"overview": "Özet",
|
||||||
"more_with": "Daha fazla {{name}}",
|
"more_with": "Daha fazla {{name}}",
|
||||||
@@ -627,10 +753,21 @@
|
|||||||
"media_options": "Medya Seçenekleri",
|
"media_options": "Medya Seçenekleri",
|
||||||
"quality": "Kalite",
|
"quality": "Kalite",
|
||||||
"audio": "Ses",
|
"audio": "Ses",
|
||||||
"subtitles": "Altyazı",
|
"subtitles": {
|
||||||
|
"label": "Subtitle",
|
||||||
|
"none": "None",
|
||||||
|
"tracks": "Tracks"
|
||||||
|
},
|
||||||
"show_more": "Daha fazla göster",
|
"show_more": "Daha fazla göster",
|
||||||
"show_less": "Daha az göster",
|
"show_less": "Daha az göster",
|
||||||
|
"left": "left",
|
||||||
|
"more_info": "More Info",
|
||||||
|
"director": "Director",
|
||||||
|
"cast": "Cast",
|
||||||
|
"technical_details": "Technical Details",
|
||||||
"appeared_in": "Şurada yer aldı",
|
"appeared_in": "Şurada yer aldı",
|
||||||
|
"movies": "Movies",
|
||||||
|
"shows": "Shows",
|
||||||
"could_not_load_item": "Öge yüklenemedi",
|
"could_not_load_item": "Öge yüklenemedi",
|
||||||
"none": "Hiçbiri",
|
"none": "Hiçbiri",
|
||||||
"download": {
|
"download": {
|
||||||
@@ -641,7 +778,13 @@
|
|||||||
"download_x_item": "{{item_count}} tane ögeyi indir",
|
"download_x_item": "{{item_count}} tane ögeyi indir",
|
||||||
"download_unwatched_only": "Yalnızca İzlenmemişler",
|
"download_unwatched_only": "Yalnızca İzlenmemişler",
|
||||||
"download_button": "İndir"
|
"download_button": "İndir"
|
||||||
}
|
},
|
||||||
|
"mark_played": "Mark as Watched",
|
||||||
|
"mark_unplayed": "Mark as Unwatched",
|
||||||
|
"resume_playback": "Resume Playback",
|
||||||
|
"resume_playback_description": "Do you want to continue where you left off or start from the beginning?",
|
||||||
|
"play_from_start": "Play from Start",
|
||||||
|
"continue_from": "Continue from {{time}}"
|
||||||
},
|
},
|
||||||
"live_tv": {
|
"live_tv": {
|
||||||
"next": "Sonraki",
|
"next": "Sonraki",
|
||||||
@@ -652,7 +795,18 @@
|
|||||||
"movies": "Filmler",
|
"movies": "Filmler",
|
||||||
"sports": "Spor",
|
"sports": "Spor",
|
||||||
"for_kids": "Çocuklar İçin",
|
"for_kids": "Çocuklar İçin",
|
||||||
"news": "Haberler"
|
"news": "Haberler",
|
||||||
|
"page_of": "Page {{current}} of {{total}}",
|
||||||
|
"no_programs": "No programs available",
|
||||||
|
"no_channels": "No channels available",
|
||||||
|
"tabs": {
|
||||||
|
"programs": "Programs",
|
||||||
|
"guide": "Guide",
|
||||||
|
"channels": "Channels",
|
||||||
|
"recordings": "Recordings",
|
||||||
|
"schedule": "Schedule",
|
||||||
|
"series": "Series"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"jellyseerr": {
|
"jellyseerr": {
|
||||||
"confirm": "Onayla",
|
"confirm": "Onayla",
|
||||||
@@ -697,6 +851,12 @@
|
|||||||
"decline": "Reddet",
|
"decline": "Reddet",
|
||||||
"requested_by": "{{user}} tarafından istendi",
|
"requested_by": "{{user}} tarafından istendi",
|
||||||
"unknown_user": "Bilinmeyen Kullanıcı",
|
"unknown_user": "Bilinmeyen Kullanıcı",
|
||||||
|
"select": "Select",
|
||||||
|
"request_all": "Request All",
|
||||||
|
"request_seasons": "Request Seasons",
|
||||||
|
"select_seasons": "Select Seasons",
|
||||||
|
"request_selected": "Request Selected",
|
||||||
|
"n_selected": "{{count}} selected",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"jellyseer_does_not_meet_requirements": "Jellyseerr sunucusu minimum sürüm gereksinimlerini karşılamıyor! Lütfen en az 2.0.0 sürümüne güncelleyin",
|
"jellyseer_does_not_meet_requirements": "Jellyseerr sunucusu minimum sürüm gereksinimlerini karşılamıyor! Lütfen en az 2.0.0 sürümüne güncelleyin",
|
||||||
"jellyseerr_test_failed": "Jellyseerr testi başarısız oldu. Lütfen tekrar deneyin.",
|
"jellyseerr_test_failed": "Jellyseerr testi başarısız oldu. Lütfen tekrar deneyin.",
|
||||||
@@ -716,7 +876,8 @@
|
|||||||
"search": "Ara",
|
"search": "Ara",
|
||||||
"library": "Kütüphane",
|
"library": "Kütüphane",
|
||||||
"custom_links": "Özel Bağlantılar",
|
"custom_links": "Özel Bağlantılar",
|
||||||
"favorites": "Favoriler"
|
"favorites": "Favoriler",
|
||||||
|
"settings": "Settings"
|
||||||
},
|
},
|
||||||
"music": {
|
"music": {
|
||||||
"title": "Müzik",
|
"title": "Müzik",
|
||||||
@@ -841,5 +1002,36 @@
|
|||||||
"show": "Bu dizide",
|
"show": "Bu dizide",
|
||||||
"all": "Bütün medyalarda (varsayılan)"
|
"all": "Bütün medyalarda (varsayılan)"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"companion_login": {
|
||||||
|
"title": "Pair with TV",
|
||||||
|
"align_qr": "Align the QR code within the frame",
|
||||||
|
"enter_code_manually": "Enter code manually",
|
||||||
|
"pairing_enter_credentials": "Enter credentials for TV",
|
||||||
|
"pairing_code_label": "Pairing code",
|
||||||
|
"server": "Server",
|
||||||
|
"authorize_button": "Authorize",
|
||||||
|
"authorizing": "Authorizing...",
|
||||||
|
"scan_again": "Scan Again",
|
||||||
|
"done": "Done",
|
||||||
|
"success_title": "Authorization Sent",
|
||||||
|
"pairing_tv_connecting": "The TV is connecting to your account",
|
||||||
|
"error_title": "Authorization Failed",
|
||||||
|
"error_invalid_qr": "Invalid QR code. Please scan the TV pairing code.",
|
||||||
|
"error_generic": "Something went wrong. Please try again.",
|
||||||
|
"error_permission_denied": "Camera permission is required to scan QR codes.",
|
||||||
|
"login_as": "Log in as {{username}}?",
|
||||||
|
"on_server": "on {{server}}",
|
||||||
|
"use_different_user": "Use a different user",
|
||||||
|
"open_settings": "Open Settings"
|
||||||
|
},
|
||||||
|
"pairing": {
|
||||||
|
"pair_with_phone": "Pair with Phone",
|
||||||
|
"pair_with_phone_title": "Login TV",
|
||||||
|
"pair_with_phone_description": "Scan the QR code displayed on your TV to log in",
|
||||||
|
"waiting_for_phone": "Waiting for phone...",
|
||||||
|
"scan_with_phone": "Scan with the Streamyfin app on your phone",
|
||||||
|
"logging_in": "Logging in...",
|
||||||
|
"logging_in_description": "Connecting to your server"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user