diff --git a/.eas/build/android-production-apk.yml b/.eas/build/android-production-apk.yml new file mode 100644 index 00000000..757192bb --- /dev/null +++ b/.eas/build/android-production-apk.yml @@ -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 diff --git a/.eas/build/android-production-tv.yml b/.eas/build/android-production-tv.yml new file mode 100644 index 00000000..a3fc9eff --- /dev/null +++ b/.eas/build/android-production-tv.yml @@ -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 diff --git a/.eas/build/android-production.yml b/.eas/build/android-production.yml new file mode 100644 index 00000000..651ff2b6 --- /dev/null +++ b/.eas/build/android-production.yml @@ -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 diff --git a/.eas/build/ios-production.yml b/.eas/build/ios-production.yml new file mode 100644 index 00000000..f9e228f7 --- /dev/null +++ b/.eas/build/ios-production.yml @@ -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 diff --git a/.gitattributes b/.gitattributes index 56dea966..4d651aeb 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,28 @@ -.modules/vlc-player/Frameworks/*.xcframework filter=lfs diff=lfs merge=lfs -text +# Normalise line endings to LF for everyone. Files are stored as LF in git and +# checked out as LF on every OS, so Windows clones stop producing CRLF churn +# (no more "LF will be replaced by CRLF" warnings) regardless of core.autocrlf. +* text=auto eol=lf + +# Windows-only scripts must stay CRLF +*.bat text eol=crlf +*.cmd text eol=crlf + +# Binary assets — never touched / never normalised +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.webp binary +*.ico binary +*.icns binary +*.ttf binary +*.otf binary +*.woff binary +*.woff2 binary +*.mp3 binary +*.mp4 binary +*.mov binary +*.pdf binary +*.keystore binary +*.jks binary +*.p12 binary diff --git a/.github/ISSUE_TEMPLATE/issue_report.yml b/.github/ISSUE_TEMPLATE/issue_report.yml index ccdb0d0e..af86644d 100644 --- a/.github/ISSUE_TEMPLATE/issue_report.yml +++ b/.github/ISSUE_TEMPLATE/issue_report.yml @@ -1,5 +1,5 @@ name: "🐛 Bug Report" -description: Create a report to help us improve +description: Create a report to help Streamyfin improve title: "[Bug]: " labels: - "🐛 bug" @@ -36,7 +36,7 @@ body: attributes: label: What happened? description: A clear and concise description of what the bug is. - placeholder: Describe what happened in detail. + placeholder: Describe what happened in detail, the more precise the better. validations: required: true @@ -67,7 +67,7 @@ body: attributes: label: Which device and operating system are you using? description: Please provide your device model and OS version - placeholder: e.g. iPhone 15 Pro, iOS 18.1.1 or Samsung Galaxy S24, Android 14 + placeholder: e.g. iPhone 17 Pro / iOS 26.5.1, Samsung Galaxy S25 / Android 16, Apple TV / tvOS 26.5 validations: required: true @@ -75,11 +75,11 @@ body: id: version attributes: label: Streamyfin Version - description: What version of Streamyfin are you running? + description: What version of Streamyfin are you using? options: - - 0.47.1 - - 0.30.2 - - older + - 0.54.1 + - 0.51.0 + - Older - TestFlight/Development build validations: required: true @@ -90,9 +90,9 @@ body: label: Jellyfin Server Information description: Please provide details about your Jellyfin server placeholder: | - - Jellyfin Server Version: e.g. 10.10.7 - - Server OS: e.g. Ubuntu 22.04, Windows 11, Docker - - Connection: e.g. Local network, Remote via domain, VPN + - Jellyfin Server Version: e.g. 10.11.10 + - Server OS: e.g. Ubuntu 26.04, Windows 11, Docker, Proxmox + - Connection: e.g. Local network, remote via domain, VPN - type: textarea id: screenshots @@ -104,7 +104,7 @@ body: id: logs attributes: label: Relevant logs (if available) - description: If you have access to app logs or crash reports, please include them here. **Remember to remove any personal information like server URLs or usernames.** + description: If you have access to app logs or crash reports, please include them here. **Remember to remove any personal information like server URL, API keys or usernames.** render: shell - type: textarea diff --git a/.github/renovate.json b/.github/renovate.json index fdbe3734..45c62042 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -44,22 +44,42 @@ ] } }, - "lockFileMaintenance": { - "vulnerabilityAlerts": { - "enabled": true, - "addLabels": ["security", "vulnerability"], - "assigneesFromCodeOwners": true, - "commitMessageSuffix": " [SECURITY]" + "vulnerabilityAlerts": { + "enabled": true, + "addLabels": ["security", "vulnerability"], + "assigneesFromCodeOwners": true, + "commitMessageSuffix": " [SECURITY]" + }, + "packageRules": [ + { + "description": "Expo SDK coherence: expo, react, react-native and Expo-managed modules are pinned by the Expo SDK and must move together (via `expo install --fix`), so do not raise individual update PRs — group them and require manual approval from the Dependency Dashboard", + "matchPackageNames": [ + "expo", + "react", + "react-dom", + "react-native", + "react-native-web", + "expo-*", + "@expo/*" + ], + "groupName": "Expo SDK", + "dependencyDashboardApproval": true }, - "packageRules": [ - { - "description": "Group minor and patch GitHub Action updates into a single PR", - "matchManagers": ["github-actions"], - "groupName": "CI dependencies", - "groupSlug": "ci-deps", - "matchUpdateTypes": ["minor", "patch", "digest", "pin"], - "automerge": true - } - ] - } + { + "description": "Group minor and patch GitHub Action updates into a single PR", + "matchManagers": ["github-actions"], + "groupName": "CI dependencies", + "groupSlug": "ci-deps", + "matchUpdateTypes": ["minor", "patch", "digest", "pin"], + "automerge": true + }, + { + "description": "androidx and other Google-hosted Maven packages resolve from Google's Maven repository (not Maven Central)", + "matchDatasources": ["maven"], + "registryUrls": [ + "https://dl.google.com/dl/android/maven2/", + "https://repo.maven.apache.org/maven2/" + ] + } + ] } diff --git a/.github/workflows/build-apps.yml b/.github/workflows/build-apps.yml index fd68e23a..c3fee61b 100644 --- a/.github/workflows/build-apps.yml +++ b/.github/workflows/build-apps.yml @@ -11,6 +11,15 @@ on: push: branches: [develop, master] +# Exposed to `expo prebuild` (app.config.js → extra.build) so Settings can show the +# branch + commit + Actions run a CI build was made from. EAS cloud builds use +# EAS_BUILD_* instead. The run number maps a sideloaded build back to its Actions +# run (artifacts + logs) without needing Expo access. +env: + EXPO_PUBLIC_GIT_BRANCH: ${{ github.head_ref || github.ref_name }} + EXPO_PUBLIC_GIT_COMMIT: ${{ github.sha }} + EXPO_PUBLIC_GIT_RUN_NUMBER: ${{ github.run_number }} + jobs: build-android-phone: if: (!contains(github.event.head_commit.message, '[skip ci]')) @@ -33,7 +42,7 @@ jobs: swap-storage: false - name: 📥 Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -116,7 +125,7 @@ jobs: swap-storage: false - name: 📥 Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -187,7 +196,7 @@ jobs: steps: - name: 📥 Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -219,10 +228,10 @@ jobs: uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1 with: # renovate: datasource=custom.xcode depName=xcode versioning=loose - xcode-version: "26.4" + xcode-version: "26.5" - name: 🏗️ Setup EAS - uses: expo/expo-github-action@b184ff86a3c926240f1b6db41764c83a01c02eef # main + uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # main with: eas-version: latest token: ${{ secrets.EXPO_TOKEN }} @@ -231,7 +240,9 @@ jobs: - name: 🚀 Build iOS app env: EXPO_TV: 0 - run: eas build -p ios --local --non-interactive + # `ci` profile (extends production, autoIncrement off): keeps CI builds out of + # the production version tier and stops them inflating the store build counter. + run: eas build -p ios --local --non-interactive --profile ci - name: 📅 Set date tag run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV @@ -252,7 +263,7 @@ jobs: steps: - name: 📥 Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -284,7 +295,7 @@ jobs: uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1 with: # renovate: datasource=custom.xcode depName=xcode versioning=loose - xcode-version: "26.4" + xcode-version: "26.5" - name: 🚀 Build iOS app env: @@ -312,7 +323,7 @@ jobs: steps: - name: 📥 Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -344,10 +355,10 @@ jobs: uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1 with: # renovate: datasource=custom.xcode depName=xcode versioning=loose - xcode-version: "26.4" + xcode-version: "26.5" - name: 🏗️ Setup EAS - uses: expo/expo-github-action@b184ff86a3c926240f1b6db41764c83a01c02eef # main + uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # main with: eas-version: latest token: ${{ secrets.EXPO_TOKEN }} @@ -356,7 +367,7 @@ jobs: - name: 🚀 Build iOS app env: EXPO_TV: 1 - run: eas build -p ios --local --non-interactive + run: eas build -p ios --local --non-interactive --profile ci_tv - name: 📅 Set date tag run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV @@ -380,7 +391,7 @@ jobs: steps: - name: 📥 Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -412,7 +423,7 @@ jobs: uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1 with: # renovate: datasource=custom.xcode depName=xcode versioning=loose - xcode-version: "26.4" + xcode-version: "26.5" - name: 🚀 Build iOS app env: diff --git a/.github/workflows/check-lockfile.yml b/.github/workflows/check-lockfile.yml index ae4c0fe0..0cb8afc3 100644 --- a/.github/workflows/check-lockfile.yml +++ b/.github/workflows/check-lockfile.yml @@ -19,7 +19,7 @@ jobs: steps: - name: 📥 Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} show-progress: false diff --git a/.github/workflows/ci-codeql.yml b/.github/workflows/ci-codeql.yml index ba1c08dc..f79cf58a 100644 --- a/.github/workflows/ci-codeql.yml +++ b/.github/workflows/ci-codeql.yml @@ -24,16 +24,16 @@ jobs: steps: - name: 📥 Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: 🏁 Initialize CodeQL - uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 + uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: languages: ${{ matrix.language }} queries: +security-extended,security-and-quality - name: 🛠️ Autobuild - uses: github/codeql-action/autobuild@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 + uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 - name: 🧪 Perform CodeQL Analysis - uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 + uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 diff --git a/.github/workflows/conflict.yml b/.github/workflows/conflict.yml index 7793851c..de854ab6 100644 --- a/.github/workflows/conflict.yml +++ b/.github/workflows/conflict.yml @@ -17,7 +17,7 @@ jobs: pull-requests: write steps: - name: 🚩 Apply merge conflict label - uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3 + uses: eps1lon/actions-label-merge-conflict@0273be72a0bbd58fcd71d0d6c02c209b50d1e5e1 # v3.1.0 with: dirtyLabel: '⚔️ merge-conflict' commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.' diff --git a/.github/workflows/crowdin.yml b/.github/workflows/crowdin.yml index c6effebf..b0ea48a2 100644 --- a/.github/workflows/crowdin.yml +++ b/.github/workflows/crowdin.yml @@ -1,51 +1,51 @@ -name: 🌐 Translation Sync - -on: - push: - branches: [develop] - paths: - - "translations/**" - - "crowdin.yml" - - "i18n.ts" - - ".github/workflows/crowdin.yml" - # Run weekly to pull new translations - schedule: - - cron: "0 2 * * 1" # Every Monday at 2 AM UTC - workflow_dispatch: - -permissions: - contents: write - pull-requests: write - -jobs: - sync-translations: - runs-on: ubuntu-latest - - steps: - - name: 📥 Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - - - name: 🌐 Sync Translations with Crowdin - uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2 - with: - upload_sources: true - upload_translations: true - download_translations: true - localization_branch_name: I10n_crowdin_translations - create_pull_request: true - pull_request_title: "feat: New Crowdin Translations" - pull_request_body: "New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)" - pull_request_base_branch_name: "develop" - pull_request_labels: "🌐 translation" - # Quality control options - skip_untranslated_strings: false - skip_untranslated_files: false - export_only_approved: false - # Commit customization - commit_message: "feat(i18n): update translations from Crowdin" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} - CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} +name: 🌐 Translation Sync + +on: + push: + branches: [develop] + paths: + - "translations/**" + - "crowdin.yml" + - "i18n.ts" + - ".github/workflows/crowdin.yml" + # Run weekly to pull new translations + schedule: + - cron: "0 2 * * 1" # Every Monday at 2 AM UTC + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + sync-translations: + runs-on: ubuntu-latest + + steps: + - name: 📥 Checkout Repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 + + - name: 🌐 Sync Translations with Crowdin + uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2 + with: + upload_sources: true + upload_translations: true + download_translations: true + localization_branch_name: I10n_crowdin_translations + create_pull_request: true + pull_request_title: "feat: New Crowdin Translations" + pull_request_body: "New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)" + pull_request_base_branch_name: "develop" + pull_request_labels: "🌐 translation" + # Quality control options + skip_untranslated_strings: false + skip_untranslated_files: false + export_only_approved: false + # Commit customization + commit_message: "feat(i18n): update translations from Crowdin" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} diff --git a/.github/workflows/detect-duplicate.yml b/.github/workflows/detect-duplicate.yml new file mode 100644 index 00000000..265f9efe --- /dev/null +++ b/.github/workflows/detect-duplicate.yml @@ -0,0 +1,38 @@ +name: 🔁 Detect Duplicate Issues + +on: + issues: + types: [opened] + +permissions: + contents: read + +concurrency: + group: detect-duplicate-${{ github.event.issue.number }} + cancel-in-progress: true + +jobs: + detect: + name: 🔍 Find similar issues + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-24.04 + permissions: + issues: write + contents: read + steps: + - name: 📥 Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: 🍞 Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: latest + + - name: 🔍 Detect duplicate issues + run: bun scripts/detect-duplicate-issue.mjs + env: + GH_TOKEN: ${{ github.token }} + GITHUB_REPOSITORY: ${{ github.repository }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + ISSUE_TITLE: ${{ github.event.issue.title }} + ISSUE_BODY: ${{ github.event.issue.body }} diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 50013ba2..8edb8916 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -51,7 +51,7 @@ jobs: contents: read steps: - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -69,7 +69,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: 🛒 Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} submodules: recursive @@ -97,10 +97,11 @@ jobs: - "check" - "format" - "typecheck" + - "i18n:check" steps: - name: "📥 Checkout PR code" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} submodules: recursive diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..c06e8b34 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,216 @@ +name: 🚀 Release (EAS build + submit) + +# On merge to main (gated by the `production` GitHub Environment approval), +# build all targets on EAS in parallel via custom bun build configs: +# 1. iOS phone → App Store (auto-submit) +# 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: + group: release-${{ github.ref }} + cancel-in-progress: false + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + approve: + name: 🔐 Approve release + runs-on: ubuntu-24.04 + environment: production + steps: + - name: ✅ Release approved + run: echo "Release approved for ${{ github.sha }}" + + build: + name: 🚀 ${{ matrix.name }} + needs: approve + runs-on: ubuntu-24.04 + permissions: + contents: read + strategy: + fail-fast: false + matrix: + include: + - name: 🍎 iOS + platform: ios + profile: production + submit: true + - name: 📺 tvOS + platform: ios + profile: production_tv + submit: true + - name: 🤖 Android AAB + platform: android + 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: + - name: 📥 Checkout code + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 + submodules: recursive + show-progress: false + + - name: 🍞 Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: latest + + - name: 💾 Cache Bun dependencies + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun-cache + + - name: 📦 Install dependencies and reload submodules + run: | + bun install --frozen-lockfile + bun run submodule-reload + + - name: 🏗️ Setup EAS + uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # main + with: + eas-version: latest + token: ${{ secrets.EXPO_TOKEN }} + eas-cache: true + + # tvOS uses credentialsSource: local — restore the gitignored + # credentials.json + cert + provisioning profiles from secrets. + - name: 🔐 Restore tvOS signing credentials + if: matrix.profile == 'production_tv' + env: + EAS_CREDENTIALS_JSON: ${{ secrets.EAS_CREDENTIALS_JSON }} + TVOS_DIST_CERT_P12_BASE64: ${{ secrets.TVOS_DIST_CERT_P12_BASE64 }} + TVOS_APP_PROFILE_BASE64: ${{ secrets.TVOS_APP_PROFILE_BASE64 }} + TVOS_TOPSHELF_PROFILE_BASE64: ${{ secrets.TVOS_TOPSHELF_PROFILE_BASE64 }} + run: | + mkdir -p certs profiles + printf '%s' "$EAS_CREDENTIALS_JSON" > credentials.json + echo "$TVOS_DIST_CERT_P12_BASE64" | base64 -d > certs/distribution.p12 + 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 + + # Android Play submit needs the Google Play service account JSON. + - name: 🔐 Restore Google Play service account + if: matrix.platform == 'android' && matrix.submit + 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 + if: matrix.platform == 'ios' + env: + APPLE_KEY_CONTENT: ${{ secrets.APPLE_KEY_CONTENT }} + run: | + if printf '%s' "$APPLE_KEY_CONTENT" | grep -q "BEGIN PRIVATE KEY"; then + printf '%s' "$APPLE_KEY_CONTENT" > "$RUNNER_TEMP/asc_api_key.p8" + else + printf '%s' "$APPLE_KEY_CONTENT" | base64 -d > "$RUNNER_TEMP/asc_api_key.p8" + fi + + # ── Submit builds: cloud build + auto-submit to the store ── + - name: 🚀 Build & submit (${{ matrix.name }}) + if: matrix.submit + env: + EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} + EXPO_ASC_API_KEY_PATH: ${{ runner.temp }}/asc_api_key.p8 + EXPO_ASC_KEY_ID: ${{ secrets.APPLE_KEY_ID }} + EXPO_ASC_ISSUER_ID: ${{ secrets.APPLE_KEY_ISSUER_ID }} + run: | + eas build \ + --platform ${{ matrix.platform }} \ + --profile ${{ matrix.profile }} \ + --auto-submit \ + --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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + 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 diff --git a/.github/workflows/trivy-scan.yml b/.github/workflows/trivy-scan.yml new file mode 100644 index 00000000..4972e14f --- /dev/null +++ b/.github/workflows/trivy-scan.yml @@ -0,0 +1,60 @@ +name: 🛡️ Trivy Security Scan + +# Filesystem scan (Streamyfin ships no container image): finds vulnerable dependencies, +# leaked secrets and misconfigurations, and reports them to GitHub code scanning. +# Runs post-merge + weekly (not on PRs — dependency-review already gates PRs, and SARIF +# upload needs a write token that fork PRs don't get). +on: + push: + branches: [develop, master] + schedule: + - cron: "50 7 * * 5" # Weekly, Friday 07:50 UTC + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: trivy-${{ github.ref }} + cancel-in-progress: true + +jobs: + trivy: + name: 🔎 Filesystem scan + runs-on: ubuntu-24.04 + permissions: + contents: read + security-events: write # upload SARIF to code scanning + steps: + - name: 📥 Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + # Rotate the DB cache weekly (matches the scheduled scan): cache hits within the week + # instead of a fresh immutable entry per run, still refreshing the DB every week. + - name: 🗓️ Compute weekly Trivy cache key + id: trivy-cache-key + run: echo "value=trivy-db-${{ runner.os }}-$(date -u +%G-%V)" >> "$GITHUB_OUTPUT" + + - name: 💾 Cache Trivy vulnerability DB + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ~/.cache/trivy + key: ${{ steps.trivy-cache-key.outputs.value }} + restore-keys: trivy-db-${{ runner.os }}- + + - name: 🔎 Run Trivy filesystem scan + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + with: + scan-type: fs + scan-ref: . + scanners: vuln,secret,misconfig + ignore-unfixed: true + severity: CRITICAL,HIGH + format: sarif + output: trivy-results.sarif + + - name: 📤 Upload results to code scanning + uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + with: + sarif_file: trivy-results.sarif + category: trivy-fs diff --git a/.github/workflows/update-issue-form.yml b/.github/workflows/update-issue-form.yml index 7cc32197..a23ecdf2 100644 --- a/.github/workflows/update-issue-form.yml +++ b/.github/workflows/update-issue-form.yml @@ -18,7 +18,7 @@ jobs: steps: - name: 📥 Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: "🟢 Setup Node.js" uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 diff --git a/.gitignore b/.gitignore index c39e191b..d46c8a6f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ # Dependencies and Package Managers node_modules/ -bun.lock bun.lockb package-lock.json @@ -18,10 +17,11 @@ web-build/ /androidmobile /androidtv -# Module-specific Builds -modules/mpv-player/android/build -modules/player/android -modules/hls-downloader/android/build +# Gradle caches (top-level + per-module native projects) +**/.gradle/ + +# Native module build outputs (any module) +modules/*/android/build/ # Generated Applications Streamyfin.app @@ -66,13 +66,15 @@ certs/ # Version and Backup Files /version-backup-* -/modules/sf-player/android/build -/modules/music-controls/android/build -modules/background-downloader/android/build/* -/modules/mpv-player/android/build # ios:unsigned-build Artifacts build/ +# but keep EAS custom build configs (the generic build/ rule above matches .eas/build/) +!.eas/build/ +!.eas/build/** .claude/ .agents/skills/** skills-lock.json + +# CI-injected Google Play service account key (written at build time) +google-service-account.json diff --git a/app.config.js b/app.config.js index 96bbd8ea..d29ddc32 100644 --- a/app.config.js +++ b/app.config.js @@ -1,3 +1,47 @@ +const { execFileSync } = require("node:child_process"); + +// Build metadata, injected into `extra.build` and read at runtime via +// expo-constants (see utils/version.ts). Sources in priority order: +// EAS cloud build → GitHub Actions → explicit EXPO_PUBLIC_* → local git → null. +const git = (args) => { + try { + return execFileSync("git", args, { stdio: ["ignore", "pipe", "ignore"] }) + .toString() + .trim(); + } catch { + return null; + } +}; + +const buildMeta = { + commit: + ( + process.env.EAS_BUILD_GIT_COMMIT_HASH || + process.env.GITHUB_SHA || + process.env.EXPO_PUBLIC_GIT_COMMIT || + git(["rev-parse", "HEAD"]) || + "" + ).slice(0, 7) || null, + branch: + process.env.EAS_BUILD_GIT_BRANCH || + process.env.GITHUB_HEAD_REF || + process.env.GITHUB_REF_NAME || + process.env.EXPO_PUBLIC_GIT_BRANCH || + git(["rev-parse", "--abbrev-ref", "HEAD"]) || + null, + profile: + process.env.EAS_BUILD_PROFILE || + process.env.EXPO_PUBLIC_BUILD_PROFILE || + null, + // GitHub Actions run number (#2098) — lets anyone map a sideloaded CI build back + // to its Actions run (artifacts + logs) without Expo access. Null outside CI. + runNumber: + process.env.GITHUB_RUN_NUMBER || + process.env.EXPO_PUBLIC_GIT_RUN_NUMBER || + null, + builtAt: new Date().toISOString(), +}; + module.exports = ({ config }) => { if (process.env.EXPO_TV !== "1") { config.plugins.push("expo-background-task"); @@ -22,6 +66,8 @@ module.exports = ({ config }) => { androidConfig.googleServicesFile = process.env.GOOGLE_SERVICES_JSON; } + config.extra = { ...config.extra, build: buildMeta }; + return { ...(Object.keys(androidConfig).length > 0 && { android: androidConfig }), ...config, diff --git a/app.json b/app.json index 6dee6c85..296d674d 100644 --- a/app.json +++ b/app.json @@ -2,7 +2,7 @@ "expo": { "name": "Streamyfin", "slug": "streamyfin", - "version": "0.54.0", + "version": "0.54.1", "orientation": "default", "icon": "./assets/images/icon.png", "scheme": "streamyfin", @@ -36,7 +36,6 @@ "appleTeamId": "MWD5K362T8" }, "android": { - "versionCode": 93, "adaptiveIcon": { "foregroundImage": "./assets/images/icon-android-plain.png", "monochromeImage": "./assets/images/icon-android-themed.png", diff --git a/app/(auth)/(tabs)/(favorites)/see-all.tsx b/app/(auth)/(tabs)/(favorites)/see-all.tsx index e3b0198a..c885afdc 100644 --- a/app/(auth)/(tabs)/(favorites)/see-all.tsx +++ b/app/(auth)/(tabs)/(favorites)/see-all.tsx @@ -161,9 +161,7 @@ export default function FavoritesSeeAllScreen() { /> {!itemType ? ( - - {t("favorites.noData", { defaultValue: "No items found." })} - + {t("favorites.noData")} ) : isLoading ? ( @@ -194,7 +192,7 @@ export default function FavoritesSeeAllScreen() { ListEmptyComponent={ - {t("home.no_items", { defaultValue: "No items" })} + {t("home.no_items")} } diff --git a/app/(auth)/(tabs)/(home)/downloads/index.tsx b/app/(auth)/(tabs)/(home)/downloads/index.tsx index 884b1fbb..68340e6b 100644 --- a/app/(auth)/(tabs)/(home)/downloads/index.tsx +++ b/app/(auth)/(tabs)/(home)/downloads/index.tsx @@ -1,4 +1,9 @@ -import { BottomSheetModal } from "@gorhom/bottom-sheet"; +import { + BottomSheetBackdrop, + type BottomSheetBackdropProps, + BottomSheetModal, + BottomSheetView, +} from "@gorhom/bottom-sheet"; import { useNavigation } from "expo-router"; import { useAtom } from "jotai"; import { useEffect, useMemo, useRef, useState } from "react"; @@ -7,6 +12,7 @@ import { Alert, Platform, ScrollView, View } from "react-native"; import { Pressable } from "react-native-gesture-handler"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { toast } from "sonner-native"; +import { Button } from "@/components/Button"; import { Text } from "@/components/common/Text"; import { TouchableItemRouter } from "@/components/common/TouchableItemRouter"; import ActiveDownloads from "@/components/downloads/ActiveDownloads"; @@ -101,7 +107,7 @@ export default function DownloadsPage() { navigation.setOptions({ headerRight: () => ( bottomSheetModalRef.current?.present()} className='px-2' > f.item) || []} /> @@ -116,7 +122,7 @@ export default function DownloadsPage() { } }, [showMigration]); - const _deleteMovies = () => + const deleteMovies = () => deleteFileByType("Movie") .then(() => toast.success( @@ -127,18 +133,18 @@ export default function DownloadsPage() { writeToLog("ERROR", reason); toast.error(t("home.downloads.toasts.failed_to_delete_all_movies")); }); - const _deleteShows = () => + const deleteShows = () => deleteFileByType("Episode") .then(() => toast.success( - t("home.downloads.toasts.deleted_all_tvseries_successfully"), + t("home.downloads.toasts.deleted_all_series_successfully"), ), ) .catch((reason) => { writeToLog("ERROR", reason); - toast.error(t("home.downloads.toasts.failed_to_delete_all_tvseries")); + toast.error(t("home.downloads.toasts.failed_to_delete_all_series")); }); - const _deleteOtherMedia = () => + const deleteOtherMedia = () => Promise.all( otherMedia .filter((item) => item.item.Type) @@ -162,6 +168,9 @@ export default function DownloadsPage() { ), ); + const deleteAllMedia = async () => + await Promise.all([deleteMovies(), deleteShows(), deleteOtherMedia()]); + return ( - {t("home.downloads.tvseries")} + {t("home.downloads.series")} @@ -256,6 +265,42 @@ export default function DownloadsPage() { )} + ( + + )} + > + + + + + {otherMedia.length > 0 && ( + + )} + + + + ); } diff --git a/app/(auth)/(tabs)/(home)/settings.tsx b/app/(auth)/(tabs)/(home)/settings.tsx index db223b2b..69b980d3 100644 --- a/app/(auth)/(tabs)/(home)/settings.tsx +++ b/app/(auth)/(tabs)/(home)/settings.tsx @@ -59,17 +59,19 @@ function SettingsMobile() { - - - - router.push("/(auth)/(tabs)/(home)/companion-login") - } - title={t("pairing.pair_with_phone")} - textColor='blue' - /> - - + {Platform.OS !== "ios" && ( + + + + router.push("/(auth)/(tabs)/(home)/companion-login") + } + title={t("pairing.pair_with_phone")} + textColor='blue' + /> + + + )} diff --git a/app/(auth)/(tabs)/(home)/settings.tv.tsx b/app/(auth)/(tabs)/(home)/settings.tv.tsx index 8fb8dcef..a9a2e2fb 100644 --- a/app/(auth)/(tabs)/(home)/settings.tv.tsx +++ b/app/(auth)/(tabs)/(home)/settings.tv.tsx @@ -179,18 +179,15 @@ export default function SettingsTV() { // Handle clearing all cache in the entire app const handleClearCache = async () => { Alert.alert( - t("home.settings.storage.clear_all_cache_confirm", "Clear All Cache?"), - t( - "home.settings.storage.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.", - ), + t("home.settings.storage.clear_all_cache_confirm"), + t("home.settings.storage.clear_all_cache_confirm_desc"), [ { - text: t("common.cancel", "Cancel"), + text: t("common.cancel"), style: "cancel", }, { - text: t("common.ok", "OK"), + text: t("common.ok"), onPress: async () => { try { // 1. Clear React Query Cache (memory & MMKV) @@ -243,11 +240,8 @@ export default function SettingsTV() { } catch (error) { console.error("Failed to clear cache:", error); Alert.alert( - t("home.settings.toasts.error_deleting_files", "Error"), - t( - "home.settings.storage.clear_all_cache_error_desc", - "An error occurred while clearing the cache.", - ), + t("home.settings.toasts.error_deleting_files"), + t("home.settings.storage.clear_all_cache_error_desc"), ); } }, diff --git a/app/(auth)/(tabs)/(home)/settings/plugins/streamystats/page.tsx b/app/(auth)/(tabs)/(home)/settings/plugins/streamystats/page.tsx index 1c4dcd19..8f0a2c93 100644 --- a/app/(auth)/(tabs)/(home)/settings/plugins/streamystats/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/plugins/streamystats/page.tsx @@ -114,7 +114,7 @@ export default function StreamystatsPage() { }; const handleRefreshFromServer = useCallback(async () => { - const newPluginSettings = await refreshStreamyfinPluginSettings(true); + const newPluginSettings = await refreshStreamyfinPluginSettings(); // Update local state with new values const newUrl = newPluginSettings?.streamyStatsServerUrl?.value || ""; setUrl(newUrl); diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/page.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/page.tsx index 519d5e5c..214564eb 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/page.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/page.tsx @@ -6,6 +6,7 @@ import { BottomSheetTextInput, BottomSheetView, } from "@gorhom/bottom-sheet"; +import type { BottomSheetModalMethods } from "@gorhom/bottom-sheet/lib/typescript/types"; import { useQuery } from "@tanstack/react-query"; import { Image } from "expo-image"; import { useLocalSearchParams, useNavigation } from "expo-router"; @@ -76,7 +77,7 @@ const MobilePage: React.FC = () => { const [issueMessage, setIssueMessage] = useState(); const [requestBody, _setRequestBody] = useState(); const [issueTypeDropdownOpen, setIssueTypeDropdownOpen] = useState(false); - const advancedReqModalRef = useRef(null); + const advancedReqModalRef = useRef(null); const bottomSheetModalRef = useRef(null); const { diff --git a/app/(auth)/(tabs)/(libraries)/_layout.tsx b/app/(auth)/(tabs)/(libraries)/_layout.tsx index acc7f817..ed31b438 100644 --- a/app/(auth)/(tabs)/(libraries)/_layout.tsx +++ b/app/(auth)/(tabs)/(libraries)/_layout.tsx @@ -166,7 +166,7 @@ export default function IndexLayout() { open={dropdownOpen} onOpenChange={setDropdownOpen} trigger={ - + { keyboardDismissMode='none' screenOptions={{ tabBarBounces: true, + tabBarActiveTintColor: "#FFFFFF", + tabBarInactiveTintColor: "#9CA3AF", tabBarLabelStyle: { fontSize: TAB_LABEL_FONT_SIZE, fontWeight: "600", diff --git a/app/(auth)/(tabs)/_layout.tsx b/app/(auth)/(tabs)/_layout.tsx index 29d3748f..53fbeb91 100644 --- a/app/(auth)/(tabs)/_layout.tsx +++ b/app/(auth)/(tabs)/_layout.tsx @@ -102,8 +102,8 @@ export default function TabLayout() { !settings?.streamyStatsServerUrl || settings?.hideWatchlistsTab, tabBarIcon: Platform.OS === "android" - ? (_e) => require("@/assets/icons/list.png") - : (_e) => ({ sfSymbol: "list.bullet.rectangle" }), + ? (_e) => require("@/assets/icons/list.star.png") + : (_e) => ({ sfSymbol: "list.star" }), }} /> require("@/assets/icons/server.rack.png") + ? (_e) => require("@/assets/icons/rectangle.stack.fill.png") : (_e) => ({ sfSymbol: "rectangle.stack.fill" }), }} /> @@ -123,8 +123,8 @@ export default function TabLayout() { tabBarItemHidden: !settings?.showCustomMenuLinks, tabBarIcon: Platform.OS === "android" - ? (_e) => require("@/assets/icons/list.png") - : (_e) => ({ sfSymbol: "list.dash.fill" }), + ? (_e) => require("@/assets/icons/link.png") + : (_e) => ({ sfSymbol: "link" }), }} /> require("@/assets/icons/gear.png") //Should maybe use other libraries to have it uniform + ? (_e) => require("@/assets/icons/gearshape.fill.png") : (_e) => ({ sfSymbol: "gearshape.fill" }), }} /> diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index 0314a6e7..f3e83f0d 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -291,6 +291,11 @@ export default function DirectPlayerPage() { }; 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(); } }, [itemId, offline, api, user?.Id]); @@ -333,6 +338,12 @@ export default function DirectPlayerPage() { 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; if (offline && downloadedItem?.mediaSource) { const url = downloadedItem.videoFilePath; @@ -405,6 +416,7 @@ export default function DirectPlayerPage() { item, user?.Id, downloadedItem, + offline, ]); useEffect(() => { @@ -444,21 +456,15 @@ export default function DirectPlayerPage() { if (!item?.Id || !stream?.sessionId || offline || !api) return; const currentTimeInTicks = msToTicks(progress.get()); - await getPlaystateApi(api).onPlaybackStopped({ - itemId: item.Id, - mediaSourceId: mediaSourceId, - positionTicks: currentTimeInTicks, - playSessionId: stream.sessionId, + await getPlaystateApi(api).reportPlaybackStopped({ + playbackStopInfo: { + ItemId: item.Id, + MediaSourceId: mediaSourceId, + PositionTicks: currentTimeInTicks, + PlaySessionId: stream.sessionId, + }, }); - }, [ - api, - item, - mediaSourceId, - stream, - progress, - offline, - revalidateProgressCache, - ]); + }, [api, item, mediaSourceId, stream, progress, offline]); const stop = useCallback(() => { // Update URL with final playback position before stopping @@ -476,9 +482,10 @@ export default function DirectPlayerPage() { useEffect(() => { const beforeRemoveListener = navigation.addListener("beforeRemove", stop); return () => { + reportPlaybackStopped(); beforeRemoveListener(); }; - }, [navigation, stop]); + }, [navigation, stop, reportPlaybackStopped]); const currentPlayStateInfo = useCallback((): | PlaybackProgressInfo diff --git a/assets/icons/gear.png b/assets/icons/gear.png deleted file mode 100644 index f5b98cf0..00000000 Binary files a/assets/icons/gear.png and /dev/null differ diff --git a/assets/icons/gearshape.fill.png b/assets/icons/gearshape.fill.png new file mode 100644 index 00000000..a3ee5bfe Binary files /dev/null and b/assets/icons/gearshape.fill.png differ diff --git a/assets/icons/heart.fill.png b/assets/icons/heart.fill.png index 25bb2527..fd868d99 100644 Binary files a/assets/icons/heart.fill.png and b/assets/icons/heart.fill.png differ diff --git a/assets/icons/heart.png b/assets/icons/heart.png deleted file mode 100644 index 96a448a7..00000000 Binary files a/assets/icons/heart.png and /dev/null differ diff --git a/assets/icons/house.fill.png b/assets/icons/house.fill.png index 9e32f71e..aa6f116c 100644 Binary files a/assets/icons/house.fill.png and b/assets/icons/house.fill.png differ diff --git a/assets/icons/jellyseerr-logo.svg b/assets/icons/jellyseerr-logo.svg deleted file mode 100644 index 1f8b997d..00000000 --- a/assets/icons/jellyseerr-logo.svg +++ /dev/null @@ -1,118 +0,0 @@ - -AAAsdGp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAACxOanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46dXVpZDpjOGFmZTAwYS1iN2JiLTRkNTUtYmUwZi1iN2Y2Mzc4NzRlYTUAAAABtGp1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAANdqdW1iAAAAJmp1bWRjYm9yABEAEIAAAKoAOJtxA2MycGEuYWN0aW9ucwAAAACpY2JvcqFnYWN0aW9uc4GjZmFjdGlvbmtjMnBhLmVkaXRlZG1zb2Z0d2FyZUFnZW50bUFkb2JlIEZpcmVmbHlxZGlnaXRhbFNvdXJjZVR5cGV4U2h0dHA6Ly9jdi5pcHRjLm9yZy9uZXdzY29kZXMvZGlnaXRhbHNvdXJjZXR5cGUvY29tcG9zaXRlV2l0aFRyYWluZWRBbGdvcml0aG1pY01lZGlhAAAArGp1bWIAAAAoanVtZGNib3IAEQAQgAAAqgA4m3EDYzJwYS5oYXNoLmRhdGEAAAAAfGNib3KlamV4Y2x1c2lvbnOBomVzdGFydBjuZmxlbmd0aBk7SGRuYW1lbmp1bWJmIG1hbmlmZXN0Y2FsZ2ZzaGEyNTZkaGFzaFggrnb/Z0LL/KWPpqmjemYRvQg3RH4cxUsaxZtMKj493SpjcGFkSQAAAAAAAAAAAAAAAgtqdW1iAAAAJGp1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0AAAAB32Nib3KoaGRjOnRpdGxlb0dlbmVyYXRlZCBJbWFnZWlkYzpmb3JtYXRtaW1hZ2Uvc3ZnK3htbGppbnN0YW5jZUlEeCx4bXA6aWlkOjJmMzZiOTBiLTUyNTctNGIzMi05NjIyLTExOGUyYjY1NTJmZW9jbGFpbV9nZW5lcmF0b3J4NkFkb2JlX0lsbHVzdHJhdG9yLzI4LjQgYWRvYmVfYzJwYS8wLjcuNiBjMnBhLXJzLzAuMjUuMnRjbGFpbV9nZW5lcmF0b3JfaW5mb4G/ZG5hbWVxQWRvYmUgSWxsdXN0cmF0b3JndmVyc2lvbmQyOC40/2lzaWduYXR1cmV4GXNlbGYjanVtYmY9YzJwYS5zaWduYXR1cmVqYXNzZXJ0aW9uc4KiY3VybHgn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AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2WQEAcNiFxc4R79ozvFI3cymplwVvAWDIKFyiBFAYVnZ4u3HEcPLDTfIt9X7Nd1vyzbJIZpVE6NOicYEaRwt+uauSMcSPsX9PHUJgyWALEQ6RHudtr57nbNIgmioCefdyEtzGbCylEalKZNWNlzjT2rgZFB1shhJ3hhVHDBPaKX2KxL3C8utMK2iBREKaVCatCmw4JVECUjwN7Qn3V347tiBf5wbCt/a+q382311bbBSW57XWiNjoek/xXArl25l6pWZSkTcShpTPT7ynjoFFRwCewR5+xU+2LKETQ4wrV3n5nK6RayHlThKGkqv3GuPOMk8ogRGaHezj/nphLuUsoIjpNA== - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/icons/link.png b/assets/icons/link.png new file mode 100644 index 00000000..95d08787 Binary files /dev/null and b/assets/icons/link.png differ diff --git a/assets/icons/list.png b/assets/icons/list.png deleted file mode 100644 index 3c548bb4..00000000 Binary files a/assets/icons/list.png and /dev/null differ diff --git a/assets/icons/list.star.png b/assets/icons/list.star.png new file mode 100644 index 00000000..cfa85c3a Binary files /dev/null and b/assets/icons/list.star.png differ diff --git a/assets/icons/magnifyingglass.png b/assets/icons/magnifyingglass.png index 5fc44c41..d62b6482 100644 Binary files a/assets/icons/magnifyingglass.png and b/assets/icons/magnifyingglass.png differ diff --git a/assets/icons/rectangle.stack.fill.png b/assets/icons/rectangle.stack.fill.png new file mode 100644 index 00000000..86460d70 Binary files /dev/null and b/assets/icons/rectangle.stack.fill.png differ diff --git a/assets/icons/seerr-logo.svg b/assets/icons/seerr-logo.svg new file mode 100644 index 00000000..a0e32e79 --- /dev/null +++ b/assets/icons/seerr-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/server.rack.png b/assets/icons/server.rack.png deleted file mode 100644 index 245e5ad2..00000000 Binary files a/assets/icons/server.rack.png and /dev/null differ diff --git a/assets/images/not-rotten-tomatoes.svg b/assets/images/not-rotten-tomatoes.svg deleted file mode 100644 index 18fa58b8..00000000 --- a/assets/images/not-rotten-tomatoes.svg +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/rotten-tomatoes.png b/assets/images/rotten-tomatoes.png deleted file mode 100644 index 341b62b0..00000000 Binary files a/assets/images/rotten-tomatoes.png and /dev/null differ diff --git a/assets/images/rt_aud_fresh.svg b/assets/images/rt_aud_fresh.svg new file mode 100644 index 00000000..f9fa2904 --- /dev/null +++ b/assets/images/rt_aud_fresh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/rt_aud_rotten.svg b/assets/images/rt_aud_rotten.svg new file mode 100644 index 00000000..cd84ac5b --- /dev/null +++ b/assets/images/rt_aud_rotten.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/rt_fresh.svg b/assets/images/rt_fresh.svg new file mode 100644 index 00000000..ed6f44d7 --- /dev/null +++ b/assets/images/rt_fresh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/rt_rotten.svg b/assets/images/rt_rotten.svg new file mode 100644 index 00000000..60ba169e --- /dev/null +++ b/assets/images/rt_rotten.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/tmdb_logo.svg b/assets/images/tmdb_logo.svg new file mode 100644 index 00000000..bdf988ba --- /dev/null +++ b/assets/images/tmdb_logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/bun.lock b/bun.lock index ed6a2e46..97ba4fa2 100644 --- a/bun.lock +++ b/bun.lock @@ -11,9 +11,10 @@ "@expo/react-native-action-sheet": "^4.1.1", "@expo/ui": "~56.0.14", "@expo/vector-icons": "^15.0.3", - "@gorhom/bottom-sheet": "5.2.8", + "@gorhom/bottom-sheet": "5.2.14", "@jellyfin/sdk": "^0.13.0", "@react-native-community/netinfo": "^12.0.0", + "@react-navigation/material-top-tabs": "7.4.28", "@react-navigation/native": "^7.2.5", "@shopify/flash-list": "2.0.2", "@tanstack/query-sync-storage-persister": "^5.100.14", @@ -83,6 +84,7 @@ "react-native-safe-area-context": "~5.7.0", "react-native-screens": "4.25.2", "react-native-svg": "15.15.4", + "react-native-tab-view": "4.3.0", "react-native-text-ticker": "^1.15.0", "react-native-track-player": "github:lovegaoshi/react-native-track-player#APM", "react-native-udp": "^4.1.7", @@ -106,7 +108,7 @@ "@types/react": "~19.2.10", "@types/react-test-renderer": "19.1.0", "cross-env": "10.1.0", - "expo-doctor": "1.19.7", + "expo-doctor": "1.19.9", "husky": "9.1.7", "lint-staged": "17.0.5", "react-test-renderer": "19.2.3", @@ -114,11 +116,6 @@ }, }, }, - "patchedDependencies": { - "react-native-ios-utilities@5.2.0": "bun-patches/react-native-ios-utilities@5.2.0.patch", - "react-native-udp@4.1.7": "bun-patches/react-native-udp@4.1.7.patch", - "react-native-bottom-tabs@1.2.0": "bun-patches/react-native-bottom-tabs@1.2.0.patch", - }, "packages": { "@adobe/css-tools": ["@adobe/css-tools@4.5.0", "", {}, "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q=="], @@ -368,7 +365,7 @@ "@expo/xcpretty": ["@expo/xcpretty@4.4.4", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "chalk": "^4.1.0", "js-yaml": "^4.1.0" }, "bin": { "excpretty": "build/cli.js" } }, "sha512-4aQzz9vgxcNXFfo/iyNgDDYfsU5XGKKxWxZopw0cVotHiW+U8IJbIxMaxsINs6bHhtkG3StKNPcOrn3eBuxKPw=="], - "@gorhom/bottom-sheet": ["@gorhom/bottom-sheet@5.2.8", "", { "dependencies": { "@gorhom/portal": "1.0.14", "invariant": "^2.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-native": "*", "react": "*", "react-native": "*", "react-native-gesture-handler": ">=2.16.1", "react-native-reanimated": ">=3.16.0 || >=4.0.0-" }, "optionalPeers": ["@types/react", "@types/react-native"] }, "sha512-+N27SMpbBxXZQ/IA2nlEV6RGxL/qSFHKfdFKcygvW+HqPG5jVNb1OqehLQsGfBP+Up42i0gW5ppI+DhpB7UCzA=="], + "@gorhom/bottom-sheet": ["@gorhom/bottom-sheet@5.2.14", "", { "dependencies": { "@gorhom/portal": "1.0.14", "invariant": "^2.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-native": "*", "react": "*", "react-native": "*", "react-native-gesture-handler": ">=2.16.1", "react-native-reanimated": ">=3.16.0 || >=4.0.0-" }, "optionalPeers": ["@types/react", "@types/react-native"] }, "sha512-uLQFlDjp9z+jrOFcMSEldPqL5JdaXL3vXOh+juhwoNvXgTsEorJLjHTugXu+YccAG/0KJnShzKCrb71MHBsvJg=="], "@gorhom/portal": ["@gorhom/portal@1.0.14", "", { "dependencies": { "nanoid": "^3.3.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-MXyL4xvCjmgaORr/rtryDNFy3kU4qUbKlwtQqqsygd0xX3mhKjOLn6mQK8wfu0RkoE0pBE0nAasRoHua+/QZ7A=="], @@ -542,6 +539,10 @@ "@react-navigation/core": ["@react-navigation/core@7.17.5", "", { "dependencies": { "@react-navigation/routers": "^7.5.5", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "query-string": "^7.1.3", "react-is": "^19.1.0", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": ">= 18.2.0" } }, "sha512-6fDCwDTWC7DJn0SDb9DJGRlipaygHIc+2elpZBJI6Crl/2Pu+Z1d6W4jMJ2gZO6iHKf+Pe5sUiQ/uwepGprZtg=="], + "@react-navigation/elements": ["@react-navigation/elements@2.9.19", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.2.5", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-gBUvCZuUkOGw1KpLQEZIkByUz8RYPwXeoA6mZFJy9K1mxd8GdqHDMFCIoB0lfPz9rgrHj99RvtdlGZ/ZzkZv2A=="], + + "@react-navigation/material-top-tabs": ["@react-navigation/material-top-tabs@7.4.28", "", { "dependencies": { "@react-navigation/elements": "^2.9.19", "color": "^4.2.3", "react-native-tab-view": "^4.3.0" }, "peerDependencies": { "@react-navigation/native": "^7.2.5", "react": ">= 18.2.0", "react-native": "*", "react-native-pager-view": ">= 6.0.0", "react-native-safe-area-context": ">= 4.0.0" } }, "sha512-WZHJSGV2PQOD2Vr9LF8apGvcsbDKukzF3Fhh8xVNIesqaSi9TPProv4dRw6YkenUkjvFVZYkOjvwAJOToePVpA=="], + "@react-navigation/native": ["@react-navigation/native@7.2.5", "", { "dependencies": { "@react-navigation/core": "^7.17.5", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*" } }, "sha512-01AAUQiiHQAfTabq+ZyU1/ZWq+AbB/J3v0CB0UTJSON6M6cuadWNsbChzrZUdqQvHrXvg96U5i2PQLJzK3+zpg=="], "@react-navigation/routers": ["@react-navigation/routers@7.5.5", "", { "dependencies": { "nanoid": "^3.3.11" } }, "sha512-9/hhMte12Kgu+pMnLfA4EWJ0OQmIEAMVMX06FPH2yGkEQSQ3JhhCN/GkcRikzQhtEi97VYYQA15umptBUShcOQ=="], @@ -968,7 +969,7 @@ "expo-device": ["expo-device@56.0.4", "", { "dependencies": { "ua-parser-js": "^0.7.33" }, "peerDependencies": { "expo": "*" } }, "sha512-ucVcGPkvBrl2QHuy7XcYex2Y6BETvJ6TREutZrwLGUDnlvbpKS8KfQoNZOpvkyo5Nmm9RrasYQ0CrXmBHho2mg=="], - "expo-doctor": ["expo-doctor@1.19.7", "", { "bin": { "expo-doctor": "bin/expo-doctor.js" } }, "sha512-pzn7QtCifRlvGIQz8k7kszeYFaI5Yn81WTHlk/20tmd3jwnXxPjlcdyhFSkuRtO2v4a9gA/6aUWVBOosfffj9w=="], + "expo-doctor": ["expo-doctor@1.19.9", "", { "bin": { "expo-doctor": "bin/expo-doctor.js" } }, "sha512-SJW5HxEDQ9f5QdFvrUwfbdJZ4HI0EAAxsrJqrHBFjKBum+uSOcEIZPLRibwNQLTHOwTO1TWNLiMlF9sDUBWeYw=="], "expo-file-system": ["expo-file-system@56.0.7", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-dcKzo8ShPloM7jgfnMcJStgQebhP8owVjCkNI/aX6NMFV1CYB8bxKGMdnzJ3mXk5nfaiW+F/lSKr2UIJ02WAUA=="], @@ -1594,9 +1595,11 @@ "react-native-svg": ["react-native-svg@15.15.4", "", { "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3", "warn-once": "0.1.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-boT/vIRgj6zZKBpfTPJJiYWMbZE9duBMOwPK6kCSTgxsS947IFMOq9OgIFkpWZTB7t229H24pDRkh3W9ZK/J1A=="], + "react-native-tab-view": ["react-native-tab-view@4.3.0", "", { "dependencies": { "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*", "react-native-pager-view": ">= 6.0.0" } }, "sha512-qPMF75uz/7+MuVG2g+YETdGMzlWZnhC6iI4h/7EBbwIBwNBIBi2z4OA6KhY3IOOBwGHXEIz5IyA6doDqifYBHg=="], + "react-native-text-ticker": ["react-native-text-ticker@1.15.0", "", {}, "sha512-d/uK+PIOhsYMy1r8h825iq/nADiHsabz3WMbRJSnkpQYn+K9aykUAXRRhu8ZbTAzk4CgnUWajJEFxS5ZDygsdg=="], - "react-native-track-player": ["react-native-track-player@github:lovegaoshi/react-native-track-player#33a3ecd", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-windows": "*", "shaka-player": "^4.7.9" }, "optionalPeers": ["react-native-windows", "shaka-player"] }, "lovegaoshi-react-native-track-player-33a3ecd"], + "react-native-track-player": ["react-native-track-player@github:lovegaoshi/react-native-track-player#33a3ecd", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-windows": "*", "shaka-player": "^4.7.9" }, "optionalPeers": ["react-native-windows", "shaka-player"] }, "lovegaoshi-react-native-track-player-33a3ecd", "sha512-vfkld2jUj7EPkAjIc/Vbx4Q4MtOOLmYtCYCE2dWJsyLnPqgj1f0xVzBxbeVP7dfT+eSh4KIXfdxESXaHgrXIlw=="], "react-native-udp": ["react-native-udp@4.1.7", "", { "dependencies": { "buffer": "^5.6.0", "events": "^3.1.0" } }, "sha512-NUE3zewu61NCdSsLlj+l0ad6qojcVEZPT4hVG/x6DU9U4iCzwtfZSASh9vm7teAcVzLkdD+cO3411LHshAi/wA=="], @@ -2006,6 +2009,10 @@ "@react-native/metro-babel-transformer/@babel/core": ["@babel/core@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw=="], + "@react-navigation/elements/color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], + + "@react-navigation/material-top-tabs/color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], + "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], @@ -2224,6 +2231,14 @@ "@react-native-community/cli-server-api/open/is-wsl": ["is-wsl@1.1.0", "", {}, "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw=="], + "@react-navigation/elements/color/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "@react-navigation/elements/color/color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + + "@react-navigation/material-top-tabs/color/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "@react-navigation/material-top-tabs/color/color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + "@testing-library/dom/pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], "ansi-fragments/slice-ansi/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], @@ -2336,6 +2351,14 @@ "@expo/package-manager/ora/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="], + "@react-navigation/elements/color/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "@react-navigation/elements/color/color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "@react-navigation/material-top-tabs/color/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "@react-navigation/material-top-tabs/color/color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "ansi-fragments/slice-ansi/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], "chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], diff --git a/components/IntroSheet.tsx b/components/IntroSheet.tsx index 0a744e32..2b04b114 100644 --- a/components/IntroSheet.tsx +++ b/components/IntroSheet.tsx @@ -89,14 +89,14 @@ export const IntroSheet = forwardRef((_, ref) => { - Jellyseerr + Seerr {t("home.intro.jellyseerr_feature_description")} diff --git a/components/PlatformDropdown.tsx b/components/PlatformDropdown.tsx index aaea71b3..5487393d 100644 --- a/components/PlatformDropdown.tsx +++ b/components/PlatformDropdown.tsx @@ -1,13 +1,7 @@ import { Ionicons } from "@expo/vector-icons"; import { BottomSheetScrollView } from "@gorhom/bottom-sheet"; -import React, { useEffect, useState } from "react"; -import { - type LayoutChangeEvent, - Platform, - StyleSheet, - TouchableOpacity, - View, -} from "react-native"; +import React, { useEffect } from "react"; +import { Platform, StyleSheet, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Text } from "@/components/common/Text"; import { useGlobalModal } from "@/providers/GlobalModalProvider"; @@ -217,24 +211,6 @@ const PlatformDropdownComponent = ({ }: PlatformDropdownProps) => { const { showModal, hideModal, isVisible } = useGlobalModal(); - // @expo/ui's (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 useEffect(() => { if (Platform.OS === "android" && controlledOpen === true) { @@ -265,25 +241,11 @@ const PlatformDropdownComponent = ({ }, [isVisible, controlledOpen, controlledOnOpenChange]); if (Platform.OS === "ios" && !Platform.isTV) { - // Pin the wrapper to the measured trigger size. @expo/ui's (SDK 55) - // fills its parent and reports its own size via setStyleSize, so it can't - // 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. + // @expo/ui's can't size to content, so an in-flow invisible copy of + // the trigger sizes the wrapper while the Host overlays the real Menu. return ( - - {/* Hidden measurer: lays the trigger out off-flow to capture its - intrinsic size. Absolutely positioned WITHOUT right/bottom so it - sizes to the trigger's content rather than to its parent. */} - + + {trigger} diff --git a/components/Ratings.tsx b/components/Ratings.tsx index 5741233f..2e06403f 100644 --- a/components/Ratings.tsx +++ b/components/Ratings.tsx @@ -40,8 +40,8 @@ export const Ratings: React.FC = ({ item, ...props }) => { = ({ [isAndroid], ); + const isPresentedRef = useRef(false); + useEffect(() => { if (visible) { bottomSheetModalRef.current?.present(); - } else { + } else if (isPresentedRef.current) { bottomSheetModalRef.current?.dismiss(); + isPresentedRef.current = false; } }, [visible]); const handleSheetChanges = useCallback( (index: number) => { - if (index === -1) { + if (index >= 0) { + isPresentedRef.current = true; + } else if (index === -1 && isPresentedRef.current) { + isPresentedRef.current = false; resetState(); onClose(); } diff --git a/components/TrackSheet.tsx b/components/TrackSheet.tsx index 4dda0564..57e4a029 100644 --- a/components/TrackSheet.tsx +++ b/components/TrackSheet.tsx @@ -63,7 +63,7 @@ export const TrackSheet: React.FC = ({ {selected === -1 && streamType === "Subtitle" ? t("common.none") - : selectedSteam?.DisplayTitle || t("common.select", "Select")} + : selectedSteam?.DisplayTitle || t("common.select")} diff --git a/components/chapters/ChapterList.tsx b/components/chapters/ChapterList.tsx index 42a90b89..e4433209 100644 --- a/components/chapters/ChapterList.tsx +++ b/components/chapters/ChapterList.tsx @@ -11,6 +11,7 @@ import { useTranslation } from "react-i18next"; import { FlatList, Modal, Pressable, StyleSheet, View } from "react-native"; import { Text } from "@/components/common/Text"; import { Colors } from "@/constants/Colors"; +import { useControlsSafeAreaInsets } from "@/hooks/useControlsSafeAreaInsets"; import { type ChapterEntry, chapterStartsMs, @@ -38,6 +39,7 @@ function ChapterListComponent({ onClose, }: ChapterListProps) { const { t } = useTranslation(); + const safeArea = useControlsSafeAreaInsets(); const listRef = useRef>(null); const entries = useMemo(() => sortedChapters(chapters), [chapters]); @@ -74,9 +76,22 @@ function ChapterListComponent({ transparent animationType='slide' onRequestClose={onClose} + // iOS defaults to portrait-only; without this it rotates the app + // back to portrait when opened from the landscape player. Android ignores it. + supportedOrientations={["portrait", "landscape"]} > - e.stopPropagation()} style={styles.sheet}> + e.stopPropagation()} + style={[ + styles.sheet, + { + marginLeft: safeArea.left, + marginRight: safeArea.right, + paddingBottom: safeArea.bottom, + }, + ]} + > {t("chapters.title")} = ({ item }) => { } /> ); diff --git a/components/common/TouchableItemRouter.tsx b/components/common/TouchableItemRouter.tsx index cc40d2dc..fed45dc9 100644 --- a/components/common/TouchableItemRouter.tsx +++ b/components/common/TouchableItemRouter.tsx @@ -2,6 +2,7 @@ import { useActionSheet } from "@expo/react-native-action-sheet"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { useSegments } from "expo-router"; import { type PropsWithChildren, useCallback } from "react"; +import { useTranslation } from "react-i18next"; import { Platform, TouchableOpacity, @@ -149,6 +150,7 @@ export const TouchableItemRouter: React.FC> = ({ children, ...props }) => { + const { t } = useTranslation(); const segments = useSegments(); const { showActionSheetWithOptions } = useActionSheet(); const markAsPlayedStatus = useMarkAsPlayed([item]); @@ -182,11 +184,13 @@ export const TouchableItemRouter: React.FC> = ({ return; const options: string[] = [ - "Mark as Played", - "Mark as Not Played", - isFavorite ? "Unmark as Favorite" : "Mark as Favorite", - ...(isOffline ? ["Delete Download"] : []), - "Cancel", + t("common.mark_as_played"), + t("common.mark_as_not_played"), + isFavorite + ? t("music.track_options.remove_from_favorites") + : t("music.track_options.add_to_favorites"), + ...(isOffline ? [t("home.downloads.delete_download")] : []), + t("common.cancel"), ]; const cancelButtonIndex = options.length - 1; const destructiveButtonIndex = isOffline @@ -219,6 +223,7 @@ export const TouchableItemRouter: React.FC> = ({ isOffline, deleteFile, item.Id, + t, ]); if ( diff --git a/components/home/Home.tsx b/components/home/Home.tsx index 637e2041..060dafed 100644 --- a/components/home/Home.tsx +++ b/components/home/Home.tsx @@ -133,7 +133,6 @@ const HomeMobile = () => { onPress={() => { router.push("/(auth)/downloads"); }} - className='ml-1.5' style={{ marginRight: Platform.OS === "android" ? 16 : 0 }} > = ({ item, ...props }) => { return { ...item, People: people } as BaseItemDto; }, [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(); + 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( (person: BaseItemPerson, idx: number, total: number) => { diff --git a/components/login/TVAddServerForm.tsx b/components/login/TVAddServerForm.tsx index 8d0168b2..cf38fa54 100644 --- a/components/login/TVAddServerForm.tsx +++ b/components/login/TVAddServerForm.tsx @@ -1,6 +1,6 @@ import { t } from "i18next"; 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 { Text } from "@/components/common/Text"; import { useScaledTVTypography } from "@/constants/TVTypography"; @@ -107,7 +107,7 @@ export const TVAddServerForm: React.FC = ({ {/* Pair with Phone */} - {onStartPairing && ( + {Platform.OS !== "ios" && onStartPairing && (