From eb8dd51b4e4393c3a7389d8258a644e6b322774a Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 31 May 2026 13:23:02 +0200 Subject: [PATCH 01/29] feat(ci): EAS build + auto-submit release workflow for main (#1616) --- .github/workflows/release.yml | 132 ++++++++++++++++++++++++++++++++++ .gitignore | 6 ++ eas.json | 11 ++- 3 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..1e9da617f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,132 @@ +name: 🚀 Release (EAS Build + Submit) + +# Cloud EAS build + auto-submit for iOS, tvOS and Android on merge to main. +# A manual approval gate (the `production` GitHub Environment) pauses the run +# before any build/submit starts. Configure required reviewers on that +# environment in repo Settings → Environments → production. + +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 }}" + + release: + 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 + - name: 📺 tvOS + platform: ios + profile: production_tv + - name: 🤖 Android + platform: android + profile: production + + steps: + - name: 📥 Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + 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@b184ff86a3c926240f1b6db41764c83a01c02eef # main + with: + eas-version: latest + token: ${{ secrets.EXPO_TOKEN }} + eas-cache: true + + # tvOS uses local credentials (EAS can't manage tvOS provisioning + # remotely, including the TopShelf extension target). Restore the + # gitignored credentials.json + cert + profiles from secrets so the + # cloud build can sign with `credentialsSource: local`. + - 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 + + # iOS + tvOS submit upload to App Store Connect with an ASC API key. + # EAS reads it from EXPO_ASC_API_KEY_PATH / EXPO_ASC_KEY_ID / + # EXPO_ASC_ISSUER_ID (set on the build step below). Write the .p8, + # tolerating either raw-PEM or base64-encoded secret content. + - 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 + + # Android submit needs a Google Play service account JSON. eas.json's + # 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 }}) + env: + 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_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 diff --git a/.gitignore b/.gitignore index c39e191b9..46328035d 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,9 @@ web-build/ /androidmobile /androidtv +# Gradle caches (top-level + per-module native projects) +**/.gradle/ + # Module-specific Builds modules/mpv-player/android/build modules/player/android @@ -76,3 +79,6 @@ 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/eas.json b/eas.json index afc774889..c4c2b0e07 100644 --- a/eas.json +++ b/eas.json @@ -1,6 +1,6 @@ { "cli": { - "version": ">= 9.1.0", + "version": ">= 16.0.0", "appVersionSource": "remote" }, "build": { @@ -52,6 +52,7 @@ } }, "production": { + "bun": "1.3.5", "environment": "production", "autoIncrement": true, "android": { @@ -59,6 +60,7 @@ } }, "production-apk": { + "bun": "1.3.5", "environment": "production", "autoIncrement": true, "android": { @@ -67,6 +69,7 @@ } }, "production-apk-tv": { + "bun": "1.3.5", "environment": "production", "autoIncrement": true, "android": { @@ -78,6 +81,7 @@ } }, "production_tv": { + "bun": "1.3.5", "environment": "production", "autoIncrement": true, "env": { @@ -93,6 +97,11 @@ "ios": { "appleTeamId": "MWD5K362T8", "ascAppId": "6593660679" + }, + "android": { + "serviceAccountKeyPath": "./google-service-account.json", + "track": "internal", + "releaseStatus": "completed" } }, "production_tv": { From 62fc6f9a70dc0eb3f570d9be6090fd6e75cc8186 Mon Sep 17 00:00:00 2001 From: Alex <111128610+Alexk2309@users.noreply.github.com> Date: Sun, 31 May 2026 22:12:13 +1000 Subject: [PATCH 02/29] fix(progress-bar): Fix progress bar not reporting watch times (#1611) Co-authored-by: Gauvain --- components/common/ProgressBar.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/components/common/ProgressBar.tsx b/components/common/ProgressBar.tsx index 23ff1249a..9c754cdc8 100644 --- a/components/common/ProgressBar.tsx +++ b/components/common/ProgressBar.tsx @@ -37,11 +37,12 @@ export const ProgressBar: React.FC = ({ item }) => { } /> ); From c981f59a50e91d57028e42cb0816f773ff7805f0 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 31 May 2026 14:58:23 +0200 Subject: [PATCH 03/29] fix(downloads): repair bottom sheets on iOS and restore downloads delete sheet Bump @gorhom/bottom-sheet 5.2.8 -> 5.2.14, which fixes BottomSheetModal present() silently no-opping under Reanimated 4 / New Architecture (SDK 56). Affected every sheet app-wide: present() was called with a valid ref but nothing rendered (not even the backdrop). Verified on the iOS simulator that the download options sheet now opens. Also restore the downloads-page delete sheet (delete movies/series/other/all) that was accidentally dropped in the Expo 54 rewrite (#1174), which left an orphaned trigger button and underscore-silenced handlers. --- app/(auth)/(tabs)/(home)/downloads/index.tsx | 55 ++++++++++++++++++-- bun.lock | 4 +- package.json | 2 +- 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/app/(auth)/(tabs)/(home)/downloads/index.tsx b/app/(auth)/(tabs)/(home)/downloads/index.tsx index 884b1fbb2..da4a8272c 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,7 +133,7 @@ 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( @@ -138,7 +144,7 @@ export default function DownloadsPage() { writeToLog("ERROR", reason); toast.error(t("home.downloads.toasts.failed_to_delete_all_tvseries")); }); - 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 ( + ( + + )} + > + + + + + {otherMedia.length > 0 && ( + + )} + + + + ); } diff --git a/bun.lock b/bun.lock index 70f387822..cf5844776 100644 --- a/bun.lock +++ b/bun.lock @@ -11,7 +11,7 @@ "@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", @@ -365,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=="], diff --git a/package.json b/package.json index 8bc2b3c64..ccabd2f47 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "@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", From 52e6f562207db74cf3f6d7c6fe29c730f89d9403 Mon Sep 17 00:00:00 2001 From: Alex <111128610+Alexk2309@users.noreply.github.com> Date: Mon, 1 Jun 2026 05:52:41 +1000 Subject: [PATCH 04/29] fix(auth): clear stored user on logout to prevent empty home on relaunch (#1622) --- providers/JellyfinProvider.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/providers/JellyfinProvider.tsx b/providers/JellyfinProvider.tsx index e6f9853ae..8608222b8 100644 --- a/providers/JellyfinProvider.tsx +++ b/providers/JellyfinProvider.tsx @@ -69,6 +69,13 @@ const initialApi = (() => { const initialUser = (() => { try { + // Only return a stored user if we also have a token. Otherwise the + // user atom would be populated while the api atom is null (e.g. after + // a logout that left stale user JSON in storage), which causes + // useProtectedRoute to keep us inside the (auth) group instead of + // redirecting to /login. + const token = storage.getString("token"); + if (!token) return null; const userStr = storage.getString("user"); if (userStr) { return JSON.parse(userStr) as UserDto; @@ -402,6 +409,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ ); storage.remove("token"); + storage.remove("user"); clearTVDiscoverySafely(); setUser(null); setApi(null); From c663bd041392767b9a1abf5aec1c81ca89b7adbd Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 31 May 2026 22:10:15 +0200 Subject: [PATCH 05/29] fix(jellyseerr): correct RequestModal ref type to fix typecheck advancedReqModalRef was typed as BottomSheetModal but RequestModal's forwardRef expects BottomSheetModalMethods, causing a TS2322 error that broke the Security & Quality Gate typecheck on develop. --- .../jellyseerr/page.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 519d5e5cc..214564ebb 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 { From 6b7ee0514f001aceda8654789ef6a7f520d568c5 Mon Sep 17 00:00:00 2001 From: Felix Schneider Date: Sun, 31 May 2026 23:45:45 +0200 Subject: [PATCH 06/29] feat(i18n): add new translations for action sheet options (#1475) Co-authored-by: lance chant <13349722+lancechant@users.noreply.github.com> Co-authored-by: Gauvain --- components/common/TouchableItemRouter.tsx | 15 ++++++++++----- translations/de.json | 3 +++ translations/en.json | 3 +++ 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/components/common/TouchableItemRouter.tsx b/components/common/TouchableItemRouter.tsx index cc40d2dc7..fed45dc99 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/translations/de.json b/translations/de.json index 87df58142..4cc148aa6 100644 --- a/translations/de.json +++ b/translations/de.json @@ -456,6 +456,7 @@ "new_app_version_requires_re_download_description": "Die neue App-Version erfordert das erneute Herunterladen von Filmen und Serien. Bitte lösche alle heruntergeladenen Elemente und starte den Download erneut.", "back": "Zurück", "delete": "Löschen", + "delete_download": "Download löschen", "something_went_wrong": "Etwas ist schiefgelaufen", "could_not_get_stream_url_from_jellyfin": "Konnte keine Stream-URL von Jellyfin erhalten", "eta": "ETA {{eta}}", @@ -498,6 +499,8 @@ "audio": "Audio", "subtitle": "Untertitel", "play": "Abspielen", + "mark_as_played": "Als gesehen markieren", + "mark_as_not_played": "Als ungesehen markieren", "none": "Keine", "track": "Spur", "cancel": "Abbrechen", diff --git a/translations/en.json b/translations/en.json index 320526063..95e4e18bf 100644 --- a/translations/en.json +++ b/translations/en.json @@ -534,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.", "back": "Back", "delete": "Delete", + "delete_download": "Delete Download", "something_went_wrong": "Something Went Wrong", "could_not_get_stream_url_from_jellyfin": "Could not get the stream URL from Jellyfin", "eta": "ETA {{eta}}", @@ -577,6 +578,8 @@ "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", From 6aa0868bfddb9f4b0eccb3ebc16bbf8d7c795aeb Mon Sep 17 00:00:00 2001 From: lance chant <13349722+lancechant@users.noreply.github.com> Date: Mon, 1 Jun 2026 09:35:19 +0200 Subject: [PATCH 07/29] fix: fixed a runtime issue for android (#1628) Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com> --- .../android/build.gradle | 40 ++++--------------- 1 file changed, 7 insertions(+), 33 deletions(-) diff --git a/modules/background-downloader/android/build.gradle b/modules/background-downloader/android/build.gradle index 1b273d728..f2987348e 100644 --- a/modules/background-downloader/android/build.gradle +++ b/modules/background-downloader/android/build.gradle @@ -1,46 +1,20 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' -} +apply plugin: 'expo-module-gradle-plugin' group = 'expo.modules.backgrounddownloader' version = '1.0.0' -def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle") -def kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.25' - -apply from: expoModulesCorePlugin - -applyKotlinExpoModulesCorePlugin() -useDefaultAndroidSdkVersions() -useCoreDependencies() -useExpoPublishing() +expoModule { + canBePublished false +} android { namespace "expo.modules.backgrounddownloader" - - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - - kotlinOptions { - jvmTarget = "17" - } - - lintOptions { - abortOnError false + defaultConfig { + versionCode 1 + versionName "1.0.0" } } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" implementation "com.squareup.okhttp3:okhttp:4.12.0" } - -tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { - kotlinOptions { - jvmTarget = "17" - } -} - From 863dffd944562a8c272048cd719a533bdb855cba Mon Sep 17 00:00:00 2001 From: Gauvain Date: Mon, 1 Jun 2026 09:37:35 +0200 Subject: [PATCH 08/29] fix(chapters): keep landscape when opening chapter list on iOS (#1624) --- components/chapters/ChapterList.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/chapters/ChapterList.tsx b/components/chapters/ChapterList.tsx index 42a90b89e..dd6ef1bd9 100644 --- a/components/chapters/ChapterList.tsx +++ b/components/chapters/ChapterList.tsx @@ -74,6 +74,9 @@ 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}> From 1d79b513f35bb6de8479ab5fb75e3df3fd0c5bec Mon Sep 17 00:00:00 2001 From: Gauvain Date: Mon, 1 Jun 2026 09:37:45 +0200 Subject: [PATCH 09/29] fix(item): dedupe top people sections by id (#1623) --- components/item/ItemPeopleSections.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/components/item/ItemPeopleSections.tsx b/components/item/ItemPeopleSections.tsx index 0b16271fc..456276da2 100644 --- a/components/item/ItemPeopleSections.tsx +++ b/components/item/ItemPeopleSections.tsx @@ -37,7 +37,20 @@ export const ItemPeopleSections: React.FC = ({ 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) => { From 21fb05658626c93b85e9672f3ebb15c8795951df Mon Sep 17 00:00:00 2001 From: Gauvain Date: Mon, 1 Jun 2026 09:46:27 +0200 Subject: [PATCH 10/29] fix(i18n): make two hardcoded titles translatable (#1627) --- components/settings/LibraryOptionsSheet.tsx | 2 +- modules/mpv-player/src/MpvPlayerView.web.tsx | 4 +++- translations/en.json | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/components/settings/LibraryOptionsSheet.tsx b/components/settings/LibraryOptionsSheet.tsx index c84989b58..e02e22fa1 100644 --- a/components/settings/LibraryOptionsSheet.tsx +++ b/components/settings/LibraryOptionsSheet.tsx @@ -229,7 +229,7 @@ export const LibraryOptionsSheet: React.FC = ({ /> - +