From 86e39c444c6e7ffba08b079aeaf5ba1141463750 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 31 May 2026 09:37:08 +0200 Subject: [PATCH 01/59] fix(ios): SDK 56 / iOS 26 EAS build fixes (SwiftUICore autolink + patch-package) (#1613) --- bun.lock | 5 ---- package.json | 7 +---- .../react-native-bottom-tabs+1.2.0.patch | 27 +++++++++---------- .../react-native-ios-utilities+5.2.0.patch | 6 ++--- .../react-native-udp+4.1.7.patch | 9 +++---- plugins/with-runtime-framework-headers.js | 22 +++++++++++++++ 6 files changed, 41 insertions(+), 35 deletions(-) rename bun-patches/react-native-bottom-tabs@1.2.0.patch => patches/react-native-bottom-tabs+1.2.0.patch (69%) rename bun-patches/react-native-ios-utilities@5.2.0.patch => patches/react-native-ios-utilities+5.2.0.patch (74%) rename bun-patches/react-native-udp@4.1.7.patch => patches/react-native-udp+4.1.7.patch (62%) diff --git a/bun.lock b/bun.lock index ed6a2e46..aa50de16 100644 --- a/bun.lock +++ b/bun.lock @@ -114,11 +114,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=="], diff --git a/package.json b/package.json index f7256b9a..74bcd674 100644 --- a/package.json +++ b/package.json @@ -162,10 +162,5 @@ }, "trustedDependencies": [ "unrs-resolver" - ], - "patchedDependencies": { - "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", - "react-native-ios-utilities@5.2.0": "bun-patches/react-native-ios-utilities@5.2.0.patch" - } + ] } diff --git a/bun-patches/react-native-bottom-tabs@1.2.0.patch b/patches/react-native-bottom-tabs+1.2.0.patch similarity index 69% rename from bun-patches/react-native-bottom-tabs@1.2.0.patch rename to patches/react-native-bottom-tabs+1.2.0.patch index 9483b873..e44815ef 100644 --- a/bun-patches/react-native-bottom-tabs@1.2.0.patch +++ b/patches/react-native-bottom-tabs+1.2.0.patch @@ -1,10 +1,7 @@ -diff --git a/node_modules/react-native-bottom-tabs/.bun-tag-b32ab1c60a5dfcf7 b/.bun-tag-b32ab1c60a5dfcf7 -new file mode 100644 -index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 -diff --git a/ios/BottomAccessoryProvider.swift b/ios/BottomAccessoryProvider.swift +diff --git a/node_modules/react-native-bottom-tabs/ios/BottomAccessoryProvider.swift b/node_modules/react-native-bottom-tabs/ios/BottomAccessoryProvider.swift index 539efee7156599e1fc795e11bf411b7dfaf12ec7..b2af39a2e6b014e9b1ae0a51b21115c19280df69 100644 ---- a/ios/BottomAccessoryProvider.swift -+++ b/ios/BottomAccessoryProvider.swift +--- a/node_modules/react-native-bottom-tabs/ios/BottomAccessoryProvider.swift ++++ b/node_modules/react-native-bottom-tabs/ios/BottomAccessoryProvider.swift @@ -8,7 +8,7 @@ import SwiftUI self.delegate = delegate } @@ -14,10 +11,10 @@ index 539efee7156599e1fc795e11bf411b7dfaf12ec7..b2af39a2e6b014e9b1ae0a51b21115c1 @available(iOS 26.0, *) public func emitPlacementChanged(_ placement: TabViewBottomAccessoryPlacement?) { var placementValue = "none" -diff --git a/ios/TabView/NewTabView.swift b/ios/TabView/NewTabView.swift +diff --git a/node_modules/react-native-bottom-tabs/ios/TabView/NewTabView.swift b/node_modules/react-native-bottom-tabs/ios/TabView/NewTabView.swift index 22c52cdf25ad0f7398d89197cb431ca8dc8e0f99..81411376e68803de8bd83515d42565cfa95daf2b 100644 ---- a/ios/TabView/NewTabView.swift -+++ b/ios/TabView/NewTabView.swift +--- a/node_modules/react-native-bottom-tabs/ios/TabView/NewTabView.swift ++++ b/node_modules/react-native-bottom-tabs/ios/TabView/NewTabView.swift @@ -78,11 +78,11 @@ struct ConditionalBottomAccessoryModifier: ViewModifier { } @@ -56,10 +53,10 @@ index 22c52cdf25ad0f7398d89197cb431ca8dc8e0f99..81411376e68803de8bd83515d42565cf } #endif + -diff --git a/ios/TabViewImpl.swift b/ios/TabViewImpl.swift +diff --git a/node_modules/react-native-bottom-tabs/ios/TabViewImpl.swift b/node_modules/react-native-bottom-tabs/ios/TabViewImpl.swift index 72938be90540ea3a483d7db9a80fb74c04d31272..277278ffdd9268a96cb09869eb1d0c0d5e6ad300 100644 ---- a/ios/TabViewImpl.swift -+++ b/ios/TabViewImpl.swift +--- a/node_modules/react-native-bottom-tabs/ios/TabViewImpl.swift ++++ b/node_modules/react-native-bottom-tabs/ios/TabViewImpl.swift @@ -281,7 +281,7 @@ extension View { @ViewBuilder @@ -69,10 +66,10 @@ index 72938be90540ea3a483d7db9a80fb74c04d31272..277278ffdd9268a96cb09869eb1d0c0d if #available(iOS 26.0, macOS 26.0, *) { if let behavior { self.tabBarMinimizeBehavior(behavior.convert()) -diff --git a/ios/TabViewProps.swift b/ios/TabViewProps.swift +diff --git a/node_modules/react-native-bottom-tabs/ios/TabViewProps.swift b/node_modules/react-native-bottom-tabs/ios/TabViewProps.swift index 9cfb29a983b34d3f84fc7a678d19ef4ff30e0325..6a5854483e66200b71722bbac12e100742222bd3 100644 ---- a/ios/TabViewProps.swift -+++ b/ios/TabViewProps.swift +--- a/node_modules/react-native-bottom-tabs/ios/TabViewProps.swift ++++ b/node_modules/react-native-bottom-tabs/ios/TabViewProps.swift @@ -6,7 +6,7 @@ internal enum MinimizeBehavior: String { case onScrollUp case onScrollDown diff --git a/bun-patches/react-native-ios-utilities@5.2.0.patch b/patches/react-native-ios-utilities+5.2.0.patch similarity index 74% rename from bun-patches/react-native-ios-utilities@5.2.0.patch rename to patches/react-native-ios-utilities+5.2.0.patch index 4659493b..4288dfcc 100644 --- a/bun-patches/react-native-ios-utilities@5.2.0.patch +++ b/patches/react-native-ios-utilities+5.2.0.patch @@ -1,7 +1,7 @@ -diff --git a/ios/Sources/Extensions+Helpers/RCTView+Helpers.swift b/ios/Sources/Extensions+Helpers/RCTView+Helpers.swift +diff --git a/node_modules/react-native-ios-utilities/ios/Sources/Extensions+Helpers/RCTView+Helpers.swift b/node_modules/react-native-ios-utilities/ios/Sources/Extensions+Helpers/RCTView+Helpers.swift index 09be306d5aa39337c5114c2ad6ba7513218e0751..24ff8ee2c36fef8632a7e012514fd04db9bf89fd 100644 ---- a/ios/Sources/Extensions+Helpers/RCTView+Helpers.swift -+++ b/ios/Sources/Extensions+Helpers/RCTView+Helpers.swift +--- a/node_modules/react-native-ios-utilities/ios/Sources/Extensions+Helpers/RCTView+Helpers.swift ++++ b/node_modules/react-native-ios-utilities/ios/Sources/Extensions+Helpers/RCTView+Helpers.swift @@ -25,15 +25,14 @@ public extension RCTView { return rootView.recursivelyFindSubview(whereType: targetType); }; diff --git a/bun-patches/react-native-udp@4.1.7.patch b/patches/react-native-udp+4.1.7.patch similarity index 62% rename from bun-patches/react-native-udp@4.1.7.patch rename to patches/react-native-udp+4.1.7.patch index 823acb86..656a73ac 100644 --- a/bun-patches/react-native-udp@4.1.7.patch +++ b/patches/react-native-udp+4.1.7.patch @@ -1,10 +1,7 @@ -diff --git a/node_modules/react-native-udp/.bun-tag-ea7df8754aa4db91 b/.bun-tag-ea7df8754aa4db91 -new file mode 100644 -index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 -diff --git a/react-native-udp.podspec b/react-native-udp.podspec +diff --git a/node_modules/react-native-udp/react-native-udp.podspec b/node_modules/react-native-udp/react-native-udp.podspec index 7450cc7d0862aadfb47d796929c801a3dc423a57..fa3e42c0152ef2d87536b8c2e484f64d525e35ec 100644 ---- a/react-native-udp.podspec -+++ b/react-native-udp.podspec +--- a/node_modules/react-native-udp/react-native-udp.podspec ++++ b/node_modules/react-native-udp/react-native-udp.podspec @@ -9,7 +9,8 @@ Pod::Spec.new do |s| s.homepage = package_json["homepage"] s.license = package_json["license"] diff --git a/plugins/with-runtime-framework-headers.js b/plugins/with-runtime-framework-headers.js index 23e7d101..8405239b 100644 --- a/plugins/with-runtime-framework-headers.js +++ b/plugins/with-runtime-framework-headers.js @@ -39,6 +39,28 @@ function buildPatch() { " end", " end", "", + " # iOS 26 / Xcode 26: the APP target itself compiles ExpoModulesProvider.swift,", + " # which imports SwiftUI-based modules (ExpoUI, ExpoGlassEffect, GlassPoster, ExpoBlur, …).", + " # That emits a `-framework SwiftUICore` autolink into the app executable's OWN object", + " # files, so the pods-only flag above is not enough — the app's link still fails with", + " # `cannot link directly with 'SwiftUICore'`. Drop the autolink on the user app target", + " # too. Phone-only — tvOS has no SwiftUICore split and must stay untouched.", + " if ENV['EXPO_TV'] != '1'", + " installer.aggregate_targets.each do |agg|", + " next unless agg.user_project", + " agg.user_project.native_targets.each do |target|", + " target.build_configurations.each do |cfg|", + " existing = cfg.build_settings['OTHER_SWIFT_FLAGS'] || '$(inherited)'", + " existing = existing.join(' ') if existing.is_a?(Array)", + " unless existing.include?('-disable-autolink-framework -Xfrontend SwiftUICore')", + " cfg.build_settings['OTHER_SWIFT_FLAGS'] = existing + ' -Xfrontend -disable-autolink-framework -Xfrontend SwiftUICore'", + " end", + " end", + " end", + " agg.user_project.save", + " end", + " end", + "", " # Safely patch RCTThirdPartyComponentsProvider.mm to avoid startup crash on unlinked Fabric components", ' filepath = "#{installer.sandbox.root}/../build/generated/ios/ReactCodegen/RCTThirdPartyComponentsProvider.mm"', " if File.exist?(filepath)", From 692ccfdb2cc18ab1fda0a0a442aa4cfb04b950e3 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 31 May 2026 09:42:09 +0200 Subject: [PATCH 02/59] fix(tvos): add arm64 UIRequiredDeviceCapabilities to Top Shelf extension App Store Connect rejected TestFlight submissions because the Top Shelf extension binary has a 64-bit slice but did not declare arm64 under UIRequiredDeviceCapabilities in its Info.plist. --- targets/StreamyfinTopShelf/Info.plist | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/targets/StreamyfinTopShelf/Info.plist b/targets/StreamyfinTopShelf/Info.plist index 184b8d3d..592d0c71 100644 --- a/targets/StreamyfinTopShelf/Info.plist +++ b/targets/StreamyfinTopShelf/Info.plist @@ -29,6 +29,10 @@ $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) + UIRequiredDeviceCapabilities + + arm64 + NSExtension NSExtensionPointIdentifier From d585b20f49c052e6536833feb38bfad2ab129d8c Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 31 May 2026 09:44:05 +0200 Subject: [PATCH 03/59] chore: version --- app.json | 4 ++-- eas.json | 8 ++++---- providers/JellyfinProvider.tsx | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app.json b/app.json index 6dee6c85..92ca6861 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,7 @@ "appleTeamId": "MWD5K362T8" }, "android": { - "versionCode": 93, + "versionCode": 94, "adaptiveIcon": { "foregroundImage": "./assets/images/icon-android-plain.png", "monochromeImage": "./assets/images/icon-android-themed.png", diff --git a/eas.json b/eas.json index 03f93389..3c125b62 100644 --- a/eas.json +++ b/eas.json @@ -52,14 +52,14 @@ }, "production": { "environment": "production", - "channel": "0.54.0", + "channel": "0.54.1", "android": { "image": "latest" } }, "production-apk": { "environment": "production", - "channel": "0.54.0", + "channel": "0.54.1", "android": { "buildType": "apk", "image": "latest" @@ -67,7 +67,7 @@ }, "production-apk-tv": { "environment": "production", - "channel": "0.54.0", + "channel": "0.54.1", "android": { "buildType": "apk", "image": "latest" @@ -78,7 +78,7 @@ }, "production_tv": { "environment": "production", - "channel": "0.54.0", + "channel": "0.54.1", "env": { "EXPO_TV": "1" }, diff --git a/providers/JellyfinProvider.tsx b/providers/JellyfinProvider.tsx index 7dff4b36..e6f9853a 100644 --- a/providers/JellyfinProvider.tsx +++ b/providers/JellyfinProvider.tsx @@ -53,7 +53,7 @@ const initialApi = (() => { const id = getOrSetDeviceId(); const deviceName = getDeviceNameSync(); const jellyfinInstance = new Jellyfin({ - clientInfo: { name: "Streamyfin", version: "0.54.0" }, + clientInfo: { name: "Streamyfin", version: "0.54.1" }, deviceInfo: { name: deviceName, id, @@ -128,7 +128,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ const id = getOrSetDeviceId(); const deviceName = getDeviceNameSync(); return new Jellyfin({ - clientInfo: { name: "Streamyfin", version: "0.54.0" }, + clientInfo: { name: "Streamyfin", version: "0.54.1" }, deviceInfo: { name: deviceName, id, @@ -162,7 +162,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ return { authorization: `MediaBrowser Client="Streamyfin", Device=${ Platform.OS === "android" ? "Android" : "iOS" - }, DeviceId="${deviceId}", Version="0.54.0"`, + }, DeviceId="${deviceId}", Version="0.54.1"`, }; }, [deviceId]); From 6b6bfd1a893677eb2e4ea4e0471fb13d37983373 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 31 May 2026 10:48:24 +0200 Subject: [PATCH 04/59] fix(player): remove white blob artifacts on vertical sliders --- components/video-player/controls/AudioSlider.tsx | 4 ++-- components/video-player/controls/BrightnessSlider.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/components/video-player/controls/AudioSlider.tsx b/components/video-player/controls/AudioSlider.tsx index 9f70fbba..31c90483 100644 --- a/components/video-player/controls/AudioSlider.tsx +++ b/components/video-player/controls/AudioSlider.tsx @@ -105,14 +105,14 @@ const AudioSlider: React.FC = ({ setVisibility }) => { maximumValue={max} thumbWidth={0} onValueChange={handleValueChange} + renderBubble={() => null} + renderThumb={() => null} containerStyle={{ borderRadius: 50, }} theme={{ minimumTrackTintColor: "#FDFDFD", maximumTrackTintColor: "#5A5A5A", - bubbleBackgroundColor: "transparent", // Hide the value bubble - bubbleTextColor: "transparent", // Hide the value text }} /> { maximumValue={max} thumbWidth={0} onValueChange={handleValueChange} + renderBubble={() => null} + renderThumb={() => null} containerStyle={{ borderRadius: 50, }} theme={{ minimumTrackTintColor: "#FDFDFD", maximumTrackTintColor: "#5A5A5A", - bubbleBackgroundColor: "transparent", // Hide the value bubble - bubbleTextColor: "transparent", // Hide the value text }} /> Date: Sun, 31 May 2026 10:50:06 +0200 Subject: [PATCH 05/59] fix(android): resolve mpv-player Kotlin smart-cast build error (#1614) --- .../src/main/java/expo/modules/mpvplayer/MPVLayerRenderer.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MPVLayerRenderer.kt b/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MPVLayerRenderer.kt index 753bfb28..8b6808fd 100644 --- a/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MPVLayerRenderer.kt +++ b/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MPVLayerRenderer.kt @@ -715,9 +715,7 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver { // dropped), so we (re)apply here for embedded and external alike. // This is what makes a carried-over subtitle show up on the next // episode without a manual re-selection. - if (initialAudioId != null && initialAudioId > 0) { - setAudioTrack(initialAudioId) - } + initialAudioId?.let { if (it > 0) setAudioTrack(it) } initialSubtitleId?.let { setSubtitleTrack(it) } ?: disableSubtitles() if (!isReadyToSeek) { From 2761de5a7498661a8b39c662bc08a5f09ff48696 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 31 May 2026 11:11:55 +0200 Subject: [PATCH 06/59] chore(eas): use remote app version source with autoIncrement Switch cli.appVersionSource to remote and enable autoIncrement on all production profiles so EAS bumps the build number every release instead of resetting to 1. Remove the dead android.versionCode from app.json and the unused EAS Update channel (no expo-updates installed). --- app.json | 1 - eas.json | 11 ++++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app.json b/app.json index 92ca6861..296d674d 100644 --- a/app.json +++ b/app.json @@ -36,7 +36,6 @@ "appleTeamId": "MWD5K362T8" }, "android": { - "versionCode": 94, "adaptiveIcon": { "foregroundImage": "./assets/images/icon-android-plain.png", "monochromeImage": "./assets/images/icon-android-themed.png", diff --git a/eas.json b/eas.json index 3c125b62..cc5eb28c 100644 --- a/eas.json +++ b/eas.json @@ -1,6 +1,7 @@ { "cli": { - "version": ">= 9.1.0" + "version": ">= 9.1.0", + "appVersionSource": "remote" }, "build": { "development": { @@ -52,14 +53,14 @@ }, "production": { "environment": "production", - "channel": "0.54.1", + "autoIncrement": true, "android": { "image": "latest" } }, "production-apk": { "environment": "production", - "channel": "0.54.1", + "autoIncrement": true, "android": { "buildType": "apk", "image": "latest" @@ -67,7 +68,7 @@ }, "production-apk-tv": { "environment": "production", - "channel": "0.54.1", + "autoIncrement": true, "android": { "buildType": "apk", "image": "latest" @@ -78,7 +79,7 @@ }, "production_tv": { "environment": "production", - "channel": "0.54.1", + "autoIncrement": true, "env": { "EXPO_TV": "1" }, From fa1c3f39479f4ea8f3ca735c5dbe9dae64d5640d Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 31 May 2026 11:22:58 +0200 Subject: [PATCH 07/59] chore(eas): pin appleTeamId and ascAppId in submit profiles Avoid the interactive Apple team picker and app-existence lookup on submit by pinning the Individual team (MWD5K362T8) and ASC App ID. --- eas.json | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/eas.json b/eas.json index cc5eb28c..afc77488 100644 --- a/eas.json +++ b/eas.json @@ -89,7 +89,17 @@ } }, "submit": { - "production": {}, - "production_tv": {} + "production": { + "ios": { + "appleTeamId": "MWD5K362T8", + "ascAppId": "6593660679" + } + }, + "production_tv": { + "ios": { + "appleTeamId": "MWD5K362T8", + "ascAppId": "6593660679" + } + } } } From dffcdef9456d325e7e70b379652c7412339e0328 Mon Sep 17 00:00:00 2001 From: Felix Schneider Date: Sun, 31 May 2026 12:10:22 +0200 Subject: [PATCH 08/59] feat(i18n): Add translation for "ends at" (#1474) Co-authored-by: Gauvain --- components/video-player/controls/TimeDisplay.tsx | 5 ++++- translations/de.json | 3 ++- translations/en.json | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/components/video-player/controls/TimeDisplay.tsx b/components/video-player/controls/TimeDisplay.tsx index 3f8cfd69..f85af634 100644 --- a/components/video-player/controls/TimeDisplay.tsx +++ b/components/video-player/controls/TimeDisplay.tsx @@ -1,4 +1,5 @@ import type { FC } from "react"; +import { useTranslation } from "react-i18next"; import { View } from "react-native"; import { Text } from "@/components/common/Text"; import { formatTimeString } from "@/utils/time"; @@ -16,6 +17,8 @@ export const TimeDisplay: FC = ({ currentTime, remainingTime, }) => { + const { t } = useTranslation(); + const getFinishTime = () => { const now = new Date(); // remainingTime is in ms @@ -37,7 +40,7 @@ export const TimeDisplay: FC = ({ -{formatTimeString(remainingTime, "ms")} - ends at {getFinishTime()} + {t("player.ends_at", { time: getFinishTime() })} diff --git a/translations/de.json b/translations/de.json index 3d3001dc..87df5814 100644 --- a/translations/de.json +++ b/translations/de.json @@ -608,7 +608,8 @@ "downloaded_file_message": "Heruntergeladene Datei abspielen?", "downloaded_file_yes": "Ja", "downloaded_file_no": "Nein", - "downloaded_file_cancel": "Abbrechen" + "downloaded_file_cancel": "Abbrechen", + "ends_at": "Endet um {{time}}" }, "item_card": { "next_up": "Als Nächstes", diff --git a/translations/en.json b/translations/en.json index 58fe4828..32052606 100644 --- a/translations/en.json +++ b/translations/en.json @@ -698,7 +698,7 @@ "downloaded_file_no": "No", "downloaded_file_cancel": "Cancel", "swipe_down_settings": "Swipe down for settings", - "ends_at": "ends at", + "ends_at": "Ends at {{time}}", "search_subtitles": "Search Subtitles", "subtitle_tracks": "Tracks", "subtitle_search": "Search & Download", From ea5a999f213105e079df7733f954fee04f6702a0 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 31 May 2026 12:40:56 +0200 Subject: [PATCH 09/59] fix(deps): declare react-native-tab-view and material-top-tabs (#1617) --- bun.lock | 28 ++++++++++++++++++++++++++++ package.json | 2 ++ 2 files changed, 30 insertions(+) diff --git a/bun.lock b/bun.lock index aa50de16..70f38782 100644 --- a/bun.lock +++ b/bun.lock @@ -14,6 +14,7 @@ "@gorhom/bottom-sheet": "5.2.8", "@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", @@ -537,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=="], @@ -1589,6 +1595,8 @@ "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"], @@ -2001,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=="], @@ -2219,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=="], @@ -2331,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/package.json b/package.json index 74bcd674..8bc2b3c6 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@gorhom/bottom-sheet": "5.2.8", "@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", @@ -104,6 +105,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", From eb8dd51b4e4393c3a7389d8258a644e6b322774a Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 31 May 2026 13:23:02 +0200 Subject: [PATCH 10/59] 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 00000000..1e9da617 --- /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 c39e191b..46328035 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 afc77488..c4c2b0e0 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 11/59] 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 23ff1249..9c754cdc 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 12/59] 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 884b1fbb..da4a8272 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 70f38782..cf584477 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 8bc2b3c6..ccabd2f4 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 13/59] 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 e6f9853a..8608222b 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 14/59] 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 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 { From 6b7ee0514f001aceda8654789ef6a7f520d568c5 Mon Sep 17 00:00:00 2001 From: Felix Schneider Date: Sun, 31 May 2026 23:45:45 +0200 Subject: [PATCH 15/59] 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 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/translations/de.json b/translations/de.json index 87df5814..4cc148aa 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 32052606..95e4e18b 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 16/59] 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 1b273d72..f2987348 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 17/59] 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 42a90b89..dd6ef1bd 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 18/59] 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 0b16271f..456276da 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 19/59] 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 c84989b5..e02e22fa 100644 --- a/components/settings/LibraryOptionsSheet.tsx +++ b/components/settings/LibraryOptionsSheet.tsx @@ -229,7 +229,7 @@ export const LibraryOptionsSheet: React.FC = ({ /> - +