crash fix test

This commit is contained in:
tapframe 2025-12-26 15:07:30 +05:30
parent 063f8a8c1b
commit 703c3e3cfb
36 changed files with 1818 additions and 3248 deletions

View file

@ -1,7 +1,11 @@
apply plugin: "com.android.application"
// @generated begin safeExtGet - expo prebuild (DO NOT MODIFY) sync-be4acad6508a6820102c74ab393bd7ab1093e6c0
def safeExtGet(prop, fallback) {
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
// @generated end safeExtGet
apply plugin: "org.jetbrains.kotlin.android"
apply plugin: "com.facebook.react"
apply plugin: "io.sentry.android.gradle"
def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()
@ -100,36 +104,6 @@ android {
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
}
// Split APKs by architecture only for smaller downloads
splits {
abi {
enable true
reset()
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
universalApk true
}
density {
enable false
}
}
// Generate unique version codes for each split APK
def abiVersionCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4]
applicationVariants.all { variant ->
variant.outputs.each { output ->
def baseVersionCode = 29 // Current versionCode 29 from defaultConfig
def abiName = output.getFilter(com.android.build.OutputFile.ABI)
def versionCode = baseVersionCode * 100 // Base multiplier
if (abiName != null) {
versionCode += abiVersionCodes.get(abiName)
}
output.versionCodeOverride = versionCode
}
}
signingConfigs {
debug {
storeFile file('debug.keystore')
@ -185,34 +159,6 @@ android {
}
}
sentry {
// Enables or disables the automatic configuration of Native Symbols
// for Sentry. This executes sentry-cli automatically so
// you don't need to do it manually.
// Default is disabled.
uploadNativeSymbols = true
// Enables or disables the automatic upload of the app's native source code to Sentry.
// This executes sentry-cli with the --include-sources param automatically so
// you don't need to do it manually.
// This option has an effect only when [uploadNativeSymbols] is enabled.
// Default is disabled.
includeNativeSources = true
// `@sentry/react-native` ships with compatible `sentry-android`
// This option would install the latest version that ships with the SDK or SAGP (Sentry Android Gradle Plugin)
// which might be incompatible with the React Native SDK
// Enable auto-installation of Sentry components (sentry-android SDK and okhttp, timber and fragment integrations).
// Default is enabled.
autoInstallation {
enabled = false
}
}
configurations.all {
exclude group: 'com.caverock', module: 'androidsvg'
}
dependencies {
// @generated begin react-native-google-cast-dependencies - expo prebuild (DO NOT MODIFY) sync-3822a3c86222e7aca74039b551612aab7e75365d
implementation "com.google.android.gms:play-services-cast-framework:${safeExtGet('castFrameworkVersion', '+')}"
@ -243,17 +189,4 @@ dependencies {
} else {
implementation jscFlavor
}
// Include only FFmpeg decoder AAR to avoid duplicates with Maven Media3
implementation files("libs/lib-decoder-ffmpeg-release.aar")
// MPV Player library
implementation files("libs/libmpv-release.aar")
// Google Cast Framework
implementation "com.google.android.gms:play-services-cast-framework:${safeExtGet('castFrameworkVersion', '+')}"
}
def safeExtGet(prop, fallback) {
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}

View file

@ -12,17 +12,3 @@
-keep class com.facebook.react.turbomodule.** { *; }
# Add any project specific keep options here:
# Media3 / ExoPlayer keep (extensions and reflection)
-keep class androidx.media3.** { *; }
-dontwarn androidx.media3.**
# FastImage / Glide ProGuard rules
-keep public class com.dylanvann.fastimage.* {*;}
-keep public class com.dylanvann.fastimage.** {*;}
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
**[] $VALUES;
public *;
}

View file

@ -1,6 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-sdk tools:overrideLibrary="dev.jdtech.mpv"/>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
@ -8,6 +6,9 @@
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
<uses-feature android:name="android.hardware.faketouch" android:required="false"/>
<uses-feature android:name="android.software.leanback" android:required="false"/>
<queries>
<intent>
<action android:name="android.intent.action.VIEW"/>
@ -23,10 +24,11 @@
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ERROR_RECOVERY_ONLY"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="30000"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://grim-reyna-tapframe-69970143.koyeb.app/api/manifest"/>
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode|locale|layoutDirection" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="unspecified">
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode|locale|layoutDirection" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>

View file

@ -1,13 +1,11 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
// @generated begin react-native-google-cast-version-import - expo prebuild (DO NOT MODIFY) sync-751dea5919495636a44001495c681e3442af2777
ext {
buildToolsVersion = "35.0.0"
minSdkVersion = 24
compileSdkVersion = 35
targetSdkVersion = 35
castFrameworkVersion = "22.1.0"
castFrameworkVersion = "+"
}
// @generated end react-native-google-cast-version-import
repositories {
google()
mavenCentral()
@ -16,7 +14,6 @@ buildscript {
classpath('com.android.tools.build:gradle')
classpath('com.facebook.react:react-native-gradle-plugin')
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
classpath("io.sentry:sentry-android-gradle-plugin:5.12.2")
}
}

View file

@ -10,7 +10,7 @@
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit

View file

@ -37,7 +37,8 @@
},
"bundleIdentifier": "com.nuvio.app",
"associatedDomains": [],
"jsEngine": "hermes"
"jsEngine": "hermes",
"appleTeamId": "8QBDZ766S3"
},
"android": {
"adaptiveIcon": {
@ -67,6 +68,7 @@
},
"owner": "nayifleo",
"plugins": [
"@react-native-tvos/config-tv",
[
"@sentry/react-native/expo",
{
@ -100,4 +102,4 @@
},
"runtimeVersion": "1.3.1"
}
}
}

View file

@ -8,9 +8,21 @@
"developmentClient": true,
"distribution": "internal"
},
"development_tv": {
"extends": "development",
"env": {
"EXPO_TV": "1"
}
},
"preview": {
"distribution": "internal"
},
"preview_tv": {
"extends": "preview",
"env": {
"EXPO_TV": "1"
}
},
"production": {
"autoIncrement": true,
"extends": "apk",
@ -20,12 +32,24 @@
"image": "latest"
}
},
"production_tv": {
"extends": "production",
"env": {
"EXPO_TV": "1"
}
},
"release": {
"distribution": "store",
"android": {
"buildType": "app-bundle"
}
},
"release_tv": {
"extends": "release",
"env": {
"EXPO_TV": "1"
}
},
"apk": {
"android": {
"buildType": "apk",
@ -36,4 +60,4 @@
"submit": {
"production": {}
}
}
}

View file

@ -1,55 +0,0 @@
//
// KSPlayerManager.m
// Nuvio
//
// Created by KSPlayer integration
//
#import <React/RCTViewManager.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
@interface RCT_EXTERN_MODULE(KSPlayerViewManager, RCTViewManager)
RCT_EXPORT_VIEW_PROPERTY(source, NSDictionary)
RCT_EXPORT_VIEW_PROPERTY(paused, BOOL)
RCT_EXPORT_VIEW_PROPERTY(volume, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(rate, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(audioTrack, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(textTrack, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(allowsExternalPlayback, BOOL)
RCT_EXPORT_VIEW_PROPERTY(usesExternalPlaybackWhileExternalScreenIsActive, BOOL)
RCT_EXPORT_VIEW_PROPERTY(subtitleBottomOffset, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(subtitleFontSize, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(resizeMode, NSString)
// Event properties
RCT_EXPORT_VIEW_PROPERTY(onLoad, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onProgress, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onBuffering, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onEnd, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onError, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onBufferingProgress, RCTDirectEventBlock)
RCT_EXTERN_METHOD(seek:(nonnull NSNumber *)node toTime:(nonnull NSNumber *)time)
RCT_EXTERN_METHOD(setSource:(nonnull NSNumber *)node source:(nonnull NSDictionary *)source)
RCT_EXTERN_METHOD(setPaused:(nonnull NSNumber *)node paused:(BOOL)paused)
RCT_EXTERN_METHOD(setVolume:(nonnull NSNumber *)node volume:(nonnull NSNumber *)volume)
RCT_EXTERN_METHOD(setPlaybackRate:(nonnull NSNumber *)node rate:(nonnull NSNumber *)rate)
RCT_EXTERN_METHOD(setAudioTrack:(nonnull NSNumber *)node trackId:(nonnull NSNumber *)trackId)
RCT_EXTERN_METHOD(setTextTrack:(nonnull NSNumber *)node trackId:(nonnull NSNumber *)trackId)
RCT_EXTERN_METHOD(getTracks:(nonnull NSNumber *)node resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(setAllowsExternalPlayback:(nonnull NSNumber *)node allows:(BOOL)allows)
RCT_EXTERN_METHOD(setUsesExternalPlaybackWhileExternalScreenIsActive:(nonnull NSNumber *)node uses:(BOOL)uses)
RCT_EXTERN_METHOD(getAirPlayState:(nonnull NSNumber *)node resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(showAirPlayPicker:(nonnull NSNumber *)node)
@end
@interface RCT_EXTERN_MODULE(KSPlayerModule, RCTEventEmitter)
RCT_EXTERN_METHOD(getTracks:(NSNumber *)nodeTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(getAirPlayState:(NSNumber *)nodeTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(showAirPlayPicker:(NSNumber *)nodeTag)
@end

View file

@ -1,71 +0,0 @@
//
// KSPlayerModule.swift
// Nuvio
//
// Created by KSPlayer integration
//
import Foundation
import KSPlayer
import React
@objc(KSPlayerModule)
class KSPlayerModule: RCTEventEmitter {
override static func requiresMainQueueSetup() -> Bool {
return true
}
override func supportedEvents() -> [String]! {
return [
"KSPlayer-onLoad",
"KSPlayer-onProgress",
"KSPlayer-onBuffering",
"KSPlayer-onEnd",
"KSPlayer-onError"
]
}
@objc func getTracks(_ nodeTag: NSNumber?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
guard let nodeTag = nodeTag else {
reject("INVALID_ARGUMENT", "nodeTag must not be nil", nil)
return
}
DispatchQueue.main.async {
if let viewManager = self.bridge.module(for: KSPlayerViewManager.self) as? KSPlayerViewManager {
viewManager.getTracks(nodeTag, resolve: resolve, reject: reject)
} else {
reject("NO_VIEW_MANAGER", "KSPlayerViewManager not found", nil)
}
}
}
@objc func getAirPlayState(_ nodeTag: NSNumber?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
guard let nodeTag = nodeTag else {
reject("INVALID_ARGUMENT", "nodeTag must not be nil", nil)
return
}
DispatchQueue.main.async {
if let viewManager = self.bridge.module(for: KSPlayerViewManager.self) as? KSPlayerViewManager {
viewManager.getAirPlayState(nodeTag, resolve: resolve, reject: reject)
} else {
reject("NO_VIEW_MANAGER", "KSPlayerViewManager not found", nil)
}
}
}
@objc func showAirPlayPicker(_ nodeTag: NSNumber?) {
guard let nodeTag = nodeTag else {
print("[KSPlayerModule] showAirPlayPicker called with nil nodeTag")
return
}
print("[KSPlayerModule] showAirPlayPicker called for nodeTag: \(nodeTag)")
DispatchQueue.main.async {
if let viewManager = self.bridge.module(for: KSPlayerViewManager.self) as? KSPlayerViewManager {
print("[KSPlayerModule] Found KSPlayerViewManager, calling showAirPlayPicker")
viewManager.showAirPlayPicker(nodeTag)
} else {
print("[KSPlayerModule] Could not find KSPlayerViewManager")
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,152 +0,0 @@
//
// KSPlayerViewManager.swift
// Nuvio
//
// Created by KSPlayer integration
//
import Foundation
import KSPlayer
import React
@objc(KSPlayerViewManager)
class KSPlayerViewManager: RCTViewManager {
// Not needed for RCTViewManager-based views; events are exported via Objective-C externs in KSPlayerManager.m
override func view() -> UIView! {
let view = KSPlayerView()
view.viewManager = self
return view
}
override static func requiresMainQueueSetup() -> Bool {
return true
}
override func constantsToExport() -> [AnyHashable : Any]! {
return [
"EventTypes": [
"onLoad": "onLoad",
"onProgress": "onProgress",
"onBuffering": "onBuffering",
"onEnd": "onEnd",
"onError": "onError",
"onBufferingProgress": "onBufferingProgress"
]
]
}
// No-op: events are sent via direct event blocks on the view
@objc func seek(_ node: NSNumber, toTime time: NSNumber) {
DispatchQueue.main.async {
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
view.seek(to: TimeInterval(truncating: time))
}
}
}
@objc func setSource(_ node: NSNumber, source: NSDictionary) {
DispatchQueue.main.async {
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
view.setSource(source)
}
}
}
@objc func setPaused(_ node: NSNumber, paused: Bool) {
DispatchQueue.main.async {
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
view.setPaused(paused)
}
}
}
@objc func setVolume(_ node: NSNumber, volume: NSNumber) {
DispatchQueue.main.async {
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
view.setVolume(Float(truncating: volume))
}
}
}
@objc func setPlaybackRate(_ node: NSNumber, rate: NSNumber) {
DispatchQueue.main.async {
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
view.setPlaybackRate(Float(truncating: rate))
}
}
}
@objc func setAudioTrack(_ node: NSNumber, trackId: NSNumber) {
DispatchQueue.main.async {
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
view.setAudioTrack(Int(truncating: trackId))
}
}
}
@objc func setTextTrack(_ node: NSNumber, trackId: NSNumber) {
NSLog("[KSPlayerViewManager] setTextTrack called - node: %@, trackId: %@", node, trackId)
DispatchQueue.main.async {
NSLog("[KSPlayerViewManager] setTextTrack on main queue - looking for view with tag: %@", node)
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
NSLog("[KSPlayerViewManager] Found view, calling setTextTrack(%d)", Int(truncating: trackId))
view.setTextTrack(Int(truncating: trackId))
} else {
NSLog("[KSPlayerViewManager] ERROR - Could not find KSPlayerView for tag: %@", node)
}
}
}
@objc func getTracks(_ node: NSNumber, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
DispatchQueue.main.async {
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
let tracks = view.getAvailableTracks()
resolve(tracks)
} else {
reject("NO_VIEW", "KSPlayerView not found", nil)
}
}
}
// AirPlay methods
@objc func setAllowsExternalPlayback(_ node: NSNumber, allows: Bool) {
DispatchQueue.main.async {
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
view.setAllowsExternalPlayback(allows)
}
}
}
@objc func setUsesExternalPlaybackWhileExternalScreenIsActive(_ node: NSNumber, uses: Bool) {
DispatchQueue.main.async {
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
view.setUsesExternalPlaybackWhileExternalScreenIsActive(uses)
}
}
}
@objc func getAirPlayState(_ node: NSNumber, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
DispatchQueue.main.async {
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
let airPlayState = view.getAirPlayState()
resolve(airPlayState)
} else {
reject("NO_VIEW", "KSPlayerView not found", nil)
}
}
}
@objc func showAirPlayPicker(_ node: NSNumber) {
print("[KSPlayerViewManager] showAirPlayPicker called for node: \(node)")
DispatchQueue.main.async {
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
print("[KSPlayerViewManager] Found KSPlayerView, calling showAirPlayPicker")
view.showAirPlayPicker()
} else {
print("[KSPlayerViewManager] Could not find KSPlayerView for node: \(node)")
}
}
}
}

View file

@ -7,15 +7,11 @@
objects = {
/* Begin PBXBuildFile section */
0FFC28FB1FEA74CCFA112268 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 49055D6E250FAFA21141FE49 /* PrivacyInfo.xcprivacy */; };
10494EC15A10E131C2A5B84D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66BF1B6C10F73045E83634B3 /* ExpoModulesProvider.swift */; };
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
2AA769395C1242F225F875AF /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E007C0BAC8C453623E81663 /* ExpoModulesProvider.swift */; };
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
564F8559E25775FFA08707DA /* libPods-Nuvio.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 18260C6F06D8D6DAF4C1D17E /* libPods-Nuvio.a */; };
9FBA88F42E86ECD700892850 /* KSPlayerViewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA88F32E86ECD700892850 /* KSPlayerViewManager.swift */; };
9FBA88F52E86ECD700892850 /* KSPlayerModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA88F12E86ECD700892850 /* KSPlayerModule.swift */; };
9FBA88F62E86ECD700892850 /* KSPlayerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA88F02E86ECD700892850 /* KSPlayerManager.m */; };
9FBA88F72E86ECD700892850 /* KSPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA88F22E86ECD700892850 /* KSPlayerView.swift */; };
5CF17A388C0A9A5F3C3CC900 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1B7D0CBC765F9DB40F1542C5 /* PrivacyInfo.xcprivacy */; };
90AAC6A10D59CBB4DE99A185 /* libPods-Nuvio.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A4DAA93D0222D02AE23FE3C /* libPods-Nuvio.a */; };
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11748412D0307B40044C1D9 /* AppDelegate.swift */; };
/* End PBXBuildFile section */
@ -24,16 +20,11 @@
13B07F961A680F5B00A75B9A /* Nuvio.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Nuvio.app; sourceTree = BUILT_PRODUCTS_DIR; };
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Nuvio/Images.xcassets; sourceTree = "<group>"; };
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Nuvio/Info.plist; sourceTree = "<group>"; };
18260C6F06D8D6DAF4C1D17E /* libPods-Nuvio.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Nuvio.a"; sourceTree = BUILT_PRODUCTS_DIR; };
2118C3C63E4B7D66EAC534DE /* Pods-Nuvio.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nuvio.debug.xcconfig"; path = "Target Support Files/Pods-Nuvio/Pods-Nuvio.debug.xcconfig"; sourceTree = "<group>"; };
49055D6E250FAFA21141FE49 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = Nuvio/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
6E007C0BAC8C453623E81663 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Nuvio/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
73BB213C2E9EEAC700EC03F8 /* NuvioRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = NuvioRelease.entitlements; path = Nuvio/NuvioRelease.entitlements; sourceTree = "<group>"; };
7F2FA62198C389C99926AA47 /* Pods-Nuvio.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nuvio.release.xcconfig"; path = "Target Support Files/Pods-Nuvio/Pods-Nuvio.release.xcconfig"; sourceTree = "<group>"; };
9FBA88F02E86ECD700892850 /* KSPlayerManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KSPlayerManager.m; sourceTree = "<group>"; };
9FBA88F12E86ECD700892850 /* KSPlayerModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KSPlayerModule.swift; sourceTree = "<group>"; };
9FBA88F22E86ECD700892850 /* KSPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KSPlayerView.swift; sourceTree = "<group>"; };
9FBA88F32E86ECD700892850 /* KSPlayerViewManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KSPlayerViewManager.swift; sourceTree = "<group>"; };
13B340DA5D39E8F6194D4A29 /* Pods-Nuvio.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nuvio.release.xcconfig"; path = "Target Support Files/Pods-Nuvio/Pods-Nuvio.release.xcconfig"; sourceTree = "<group>"; };
1B7D0CBC765F9DB40F1542C5 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = Nuvio/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
59D018C54181F6CC8CB0057C /* Pods-Nuvio.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nuvio.debug.xcconfig"; path = "Target Support Files/Pods-Nuvio/Pods-Nuvio.debug.xcconfig"; sourceTree = "<group>"; };
66BF1B6C10F73045E83634B3 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Nuvio/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
9A4DAA93D0222D02AE23FE3C /* libPods-Nuvio.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Nuvio.a"; sourceTree = BUILT_PRODUCTS_DIR; };
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = Nuvio/SplashScreen.storyboard; sourceTree = "<group>"; };
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
@ -46,7 +37,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
564F8559E25775FFA08707DA /* libPods-Nuvio.a in Frameworks */,
90AAC6A10D59CBB4DE99A185 /* libPods-Nuvio.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -56,18 +47,13 @@
13B07FAE1A68108700A75B9A /* Nuvio */ = {
isa = PBXGroup;
children = (
73BB213C2E9EEAC700EC03F8 /* NuvioRelease.entitlements */,
F11748412D0307B40044C1D9 /* AppDelegate.swift */,
F11748442D0722820044C1D9 /* Nuvio-Bridging-Header.h */,
9FBA88F02E86ECD700892850 /* KSPlayerManager.m */,
9FBA88F12E86ECD700892850 /* KSPlayerModule.swift */,
9FBA88F22E86ECD700892850 /* KSPlayerView.swift */,
9FBA88F32E86ECD700892850 /* KSPlayerViewManager.swift */,
BB2F792B24A3F905000567C9 /* Supporting */,
13B07FB51A68108700A75B9A /* Images.xcassets */,
13B07FB61A68108700A75B9A /* Info.plist */,
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
49055D6E250FAFA21141FE49 /* PrivacyInfo.xcprivacy */,
1B7D0CBC765F9DB40F1542C5 /* PrivacyInfo.xcprivacy */,
);
name = Nuvio;
sourceTree = "<group>";
@ -76,15 +62,25 @@
isa = PBXGroup;
children = (
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
18260C6F06D8D6DAF4C1D17E /* libPods-Nuvio.a */,
9A4DAA93D0222D02AE23FE3C /* libPods-Nuvio.a */,
);
name = Frameworks;
sourceTree = "<group>";
};
358C5C99C443A921C8EEDDC8 /* ExpoModulesProviders */ = {
7DE8F3BB285224D25F88E2AF /* Pods */ = {
isa = PBXGroup;
children = (
ECB31D9B6FF08C7E8E875650 /* Nuvio */,
59D018C54181F6CC8CB0057C /* Pods-Nuvio.debug.xcconfig */,
13B340DA5D39E8F6194D4A29 /* Pods-Nuvio.release.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
7F73664F3B4B3A59A03D5045 /* ExpoModulesProviders */ = {
isa = PBXGroup;
children = (
97D6A03D95036375A7C4B3D0 /* Nuvio */,
);
name = ExpoModulesProviders;
sourceTree = "<group>";
@ -103,8 +99,8 @@
832341AE1AAA6A7D00B99B32 /* Libraries */,
83CBBA001A601CBA00E9B192 /* Products */,
2D16E6871FA4F8E400B85C8A /* Frameworks */,
D90A3959C97EE9926C513293 /* Pods */,
358C5C99C443A921C8EEDDC8 /* ExpoModulesProviders */,
7DE8F3BB285224D25F88E2AF /* Pods */,
7F73664F3B4B3A59A03D5045 /* ExpoModulesProviders */,
);
indentWidth = 2;
sourceTree = "<group>";
@ -119,6 +115,14 @@
name = Products;
sourceTree = "<group>";
};
97D6A03D95036375A7C4B3D0 /* Nuvio */ = {
isa = PBXGroup;
children = (
66BF1B6C10F73045E83634B3 /* ExpoModulesProvider.swift */,
);
name = Nuvio;
sourceTree = "<group>";
};
BB2F792B24A3F905000567C9 /* Supporting */ = {
isa = PBXGroup;
children = (
@ -128,23 +132,6 @@
path = Nuvio/Supporting;
sourceTree = "<group>";
};
D90A3959C97EE9926C513293 /* Pods */ = {
isa = PBXGroup;
children = (
2118C3C63E4B7D66EAC534DE /* Pods-Nuvio.debug.xcconfig */,
7F2FA62198C389C99926AA47 /* Pods-Nuvio.release.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
ECB31D9B6FF08C7E8E875650 /* Nuvio */ = {
isa = PBXGroup;
children = (
6E007C0BAC8C453623E81663 /* ExpoModulesProvider.swift */,
);
name = Nuvio;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -152,15 +139,15 @@
isa = PBXNativeTarget;
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Nuvio" */;
buildPhases = (
4A10611824FCBAA4C1793637 /* [CP] Check Pods Manifest.lock */,
99A79B70155E84EE1FB7F466 /* [Expo] Configure project */,
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */,
C1F70A9254487D3D09AEC536 /* [Expo] Configure project */,
13B07F871A680F5B00A75B9A /* Sources */,
13B07F8C1A680F5B00A75B9A /* Frameworks */,
13B07F8E1A680F5B00A75B9A /* Resources */,
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
9B977D89FE30470F8C59964C /* Upload Debug Symbols to Sentry */,
EE80421364369BBCA82253B9 /* [CP] Embed Pods Frameworks */,
778B42B39FEE5454E4D24252 /* [CP] Copy Pods Resources */,
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */,
BC6182A9E2AB4627A4DEC380 /* Upload Debug Symbols to Sentry */,
21592BEE6E404328F78AD757 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
@ -181,6 +168,8 @@
TargetAttributes = {
13B07F861A680F5B00A75B9A = {
LastSwiftMigration = 1250;
DevelopmentTeam = "8QBDZ766S3";
ProvisioningStyle = Automatic;
};
};
};
@ -210,7 +199,7 @@
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */,
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
0FFC28FB1FEA74CCFA112268 /* PrivacyInfo.xcprivacy in Resources */,
5CF17A388C0A9A5F3C3CC900 /* PrivacyInfo.xcprivacy in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -234,7 +223,7 @@
shellPath = /bin/sh;
shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n# Source .xcode.env.updates if it exists to allow\n# SKIP_BUNDLING to be unset if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.updates\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.updates\"\nfi\n# Source local changes to allow overrides\n# if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n/bin/sh `\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode.sh'\"` `\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n";
};
4A10611824FCBAA4C1793637 /* [CP] Check Pods Manifest.lock */ = {
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@ -256,7 +245,29 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
778B42B39FEE5454E4D24252 /* [CP] Copy Pods Resources */ = {
21592BEE6E404328F78AD757 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Nuvio/Pods-Nuvio-frameworks.sh",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/React-Core-prebuilt/React.framework/React",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ReactNativeDependencies/ReactNativeDependencies.framework/ReactNativeDependencies",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactNativeDependencies.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Nuvio/Pods-Nuvio-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@ -266,14 +277,11 @@
"${PODS_CONFIGURATION_BUILD_DIR}/EXApplication/ExpoApplication_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXUpdates/EXUpdates.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoDevice/ExpoDevice_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoLocalization/ExpoLocalization_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/KSPlayer/KSPlayer_KSPlayer.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/AntDesign.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Entypo.ttf",
@ -301,14 +309,6 @@
"${PODS_CONFIGURATION_BUILD_DIR}/Sentry/Sentry.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-launcher/EXDevLauncher.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-menu/EXDevMenu.bundle",
"${PODS_ROOT}/google-cast-sdk/GoogleCastSDK-ios-4.8.4_static_xcframework/GoogleCast.xcframework/ios-arm64/GoogleCast.framework/GoogleCastCoreResources.bundle",
"${PODS_ROOT}/google-cast-sdk/GoogleCastSDK-ios-4.8.4_static_xcframework/GoogleCast.xcframework/ios-arm64/GoogleCast.framework/GoogleCastUIResources.bundle",
"${PODS_ROOT}/google-cast-sdk/GoogleCastSDK-ios-4.8.4_static_xcframework/GoogleCast.xcframework/ios-arm64/GoogleCast.framework/GoogleCastOptionalUIResources.bundle",
"${PODS_ROOT}/google-cast-sdk/GoogleCastSDK-ios-4.8.4_static_xcframework/GoogleCast.xcframework/ios-arm64/GoogleCast.framework/MaterialDialogs.bundle",
"${PODS_ROOT}/google-cast-sdk/GoogleCastSDK-ios-4.8.4_static_xcframework/GoogleCast.xcframework/ios-arm64/GoogleCast.framework/GoogleSansBold.bundle",
"${PODS_ROOT}/google-cast-sdk/GoogleCastSDK-ios-4.8.4_static_xcframework/GoogleCast.xcframework/ios-arm64/GoogleCast.framework/GoogleSansMedium.bundle",
"${PODS_ROOT}/google-cast-sdk/GoogleCastSDK-ios-4.8.4_static_xcframework/GoogleCast.xcframework/ios-arm64/GoogleCast.framework/GoogleSansRegular.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/google-cast-sdk/GoogleCast.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/lottie-ios/LottiePrivacyInfo.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/lottie-react-native/Lottie_React_Native_Privacy.bundle",
);
@ -317,14 +317,11 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoApplication_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXUpdates.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoDevice_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoLocalization_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/KSPlayer_KSPlayer.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AntDesign.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Entypo.ttf",
@ -352,14 +349,6 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Sentry.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXDevLauncher.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXDevMenu.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleCastCoreResources.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleCastUIResources.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleCastOptionalUIResources.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MaterialDialogs.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSansBold.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSansMedium.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSansRegular.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleCast.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/LottiePrivacyInfo.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Lottie_React_Native_Privacy.bundle",
);
@ -368,7 +357,21 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Nuvio/Pods-Nuvio-resources.sh\"\n";
showEnvVarsInLog = 0;
};
99A79B70155E84EE1FB7F466 /* [Expo] Configure project */ = {
BC6182A9E2AB4627A4DEC380 /* Upload Debug Symbols to Sentry */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Upload Debug Symbols to Sentry";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh `${NODE_BINARY:-node} --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode-debug-files.sh'\"`";
};
C1F70A9254487D3D09AEC536 /* [Expo] Configure project */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
@ -392,42 +395,6 @@
shellPath = /bin/sh;
shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-Nuvio/expo-configure-project.sh\"\n";
};
9B977D89FE30470F8C59964C /* Upload Debug Symbols to Sentry */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Upload Debug Symbols to Sentry";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh `${NODE_BINARY:-node} --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode-debug-files.sh'\"`";
};
EE80421364369BBCA82253B9 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Nuvio/Pods-Nuvio-frameworks.sh",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/React-Core-prebuilt/React.framework/React",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ReactNativeDependencies/ReactNativeDependencies.framework/ReactNativeDependencies",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactNativeDependencies.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Nuvio/Pods-Nuvio-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@ -436,11 +403,7 @@
buildActionMask = 2147483647;
files = (
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */,
9FBA88F42E86ECD700892850 /* KSPlayerViewManager.swift in Sources */,
9FBA88F52E86ECD700892850 /* KSPlayerModule.swift in Sources */,
9FBA88F62E86ECD700892850 /* KSPlayerManager.m in Sources */,
9FBA88F72E86ECD700892850 /* KSPlayerView.swift in Sources */,
2AA769395C1242F225F875AF /* ExpoModulesProvider.swift in Sources */,
10494EC15A10E131C2A5B84D /* ExpoModulesProvider.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -449,13 +412,12 @@
/* Begin XCBuildConfiguration section */
13B07F941A680F5B00A75B9A /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 2118C3C63E4B7D66EAC534DE /* Pods-Nuvio.debug.xcconfig */;
baseConfigurationReference = 59D018C54181F6CC8CB0057C /* Pods-Nuvio.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Nuvio/Nuvio.entitlements;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 8QBDZ766S3;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
@ -476,24 +438,27 @@
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app;
PRODUCT_NAME = Nuvio;
SUPPORTS_MACCATALYST = YES;
SDKROOT = appletvos;
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TARGETED_DEVICE_FAMILY = 3;
TVOS_DEPLOYMENT_TARGET = 15.1;
VERSIONING_SYSTEM = "apple-generic";
DEVELOPMENT_TEAM = "8QBDZ766S3";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
};
name = Debug;
};
13B07F951A680F5B00A75B9A /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7F2FA62198C389C99926AA47 /* Pods-Nuvio.release.xcconfig */;
baseConfigurationReference = 13B340DA5D39E8F6194D4A29 /* Pods-Nuvio.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Nuvio/NuvioRelease.entitlements;
CODE_SIGN_ENTITLEMENTS = Nuvio/Nuvio.entitlements;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 8QBDZ766S3;
INFOPLIST_FILE = Nuvio/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
@ -507,13 +472,17 @@
"-lc++",
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.hub;
PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app;
PRODUCT_NAME = Nuvio;
SUPPORTS_MACCATALYST = YES;
SDKROOT = appletvos;
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TARGETED_DEVICE_FAMILY = 3;
TVOS_DEPLOYMENT_TARGET = 15.1;
VERSIONING_SYSTEM = "apple-generic";
DEVELOPMENT_TEAM = "8QBDZ766S3";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
};
name = Release;
};
@ -573,7 +542,7 @@
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SDKROOT = appletvos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
USE_HERMES = true;
@ -628,7 +597,7 @@
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
MTL_ENABLE_DEBUG_INFO = NO;
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SDKROOT = appletvos;
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
USE_HERMES = true;
VALIDATE_PRODUCT = YES;

View file

@ -1,110 +1,103 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Nuvio</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.2.10</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>nuvio</string>
<string>com.nuvio.app</string>
</array>
</dict>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>exp+nuvio</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>29</string>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSBonjourServices</key>
<array>
<string>_http._tcp</string>
<string>_googlecast._tcp</string>
<string>_CC1AD845._googlecast._tcp</string>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your local network</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app does not require microphone access.</string>
<key>RCTNewArchEnabled</key>
<true/>
<key>RCTRootViewBackgroundColor</key>
<integer>4278322180</integer>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
<key>UIFileSharingEnabled</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>SplashScreen</string>
<key>UILaunchScreen</key>
<dict>
<key>UIColorName</key>
<string>SplashScreenBackground</string>
<key>UIImageName</key>
<string>SplashScreenLegacy</string>
</dict>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UIRequiresFullScreen</key>
<false/>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleDefault</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Dark</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Nuvio</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.3.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>nuvio</string>
<string>com.nuvio.app</string>
</array>
</dict>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>exp+nuvio</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>29</string>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSBonjourServices</key>
<array>
<string>_http._tcp</string>
<string>_googlecast._tcp</string>
<string>_CC1AD845._googlecast._tcp</string>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>Nuvio uses the local network to discover Cast-enabled devices on your WiFi network and to connect to local media servers.</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app does not require microphone access.</string>
<key>RCTNewArchEnabled</key>
<true/>
<key>RCTRootViewBackgroundColor</key>
<integer>4278322180</integer>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
<key>UIFileSharingEnabled</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>SplashScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UIRequiresFullScreen</key>
<false/>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleDefault</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Dark</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>

View file

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.associated-domains</key>
<array/>
</dict>
</plist>

View file

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.associated-domains</key>
<array/>
</dict>
</plist>

View file

@ -20,7 +20,6 @@
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
<string>C56D.1</string>
</array>
</dict>
<dict>

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24093.7" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="EXPO-VIEWCONTROLLER-1">
<document type="com.apple.InterfaceBuilder.AppleTV.Storyboard" version="3.0" toolsVersion="24093.7" targetRuntime="AppleTV" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="EXPO-VIEWCONTROLLER-1">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<deployment identifier="tvOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24053.1"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
@ -15,7 +15,7 @@
<viewController storyboardIdentifier="SplashScreenViewController" id="EXPO-VIEWCONTROLLER-1" sceneMemberID="viewController">
<view key="view" userInteractionEnabled="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="EXPO-ContainerView" userLabel="ContainerView">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<imageView id="EXPO-SplashScreen" userLabel="SplashScreenLegacy" image="SplashScreenLegacy" contentMode="scaleAspectFit" clipsSubviews="true" userInteractionEnabled="false" translatesAutoresizingMaskIntoConstraints="false">
<rect key="frame" x="0" y="0" width="414" height="736"/>

View file

@ -9,7 +9,7 @@
<key>EXUpdatesLaunchWaitMs</key>
<integer>30000</integer>
<key>EXUpdatesRuntimeVersion</key>
<string>1.2.11</string>
<string>1.3.1</string>
<key>EXUpdatesURL</key>
<string>https://grim-reyna-tapframe-69970143.koyeb.app/api/manifest</string>
</dict>

View file

@ -16,12 +16,12 @@ ENV['RCT_NEW_ARCH_ENABLED'] ||= '0' if podfile_properties['newArchEnabled'] == '
ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] ||= podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
ENV['RCT_USE_RN_DEP'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false'
ENV['RCT_USE_PREBUILT_RNCORE'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false'
platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'
platform :tvos, podfile_properties['ios.deploymentTarget'] || '15.1'
prepare_react_native_project!
target 'Nuvio' do
use_expo_modules!(exclude: ['expo-libvlc-player'])
use_expo_modules!
if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1'
config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"];
@ -49,12 +49,6 @@ target 'Nuvio' do
:privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false',
)
# KSPlayer dependencies
pod 'KSPlayer', :git => 'https://github.com/kingslay/KSPlayer.git', :branch => 'main'
pod 'DisplayCriteria', :git => 'https://github.com/kingslay/KSPlayer.git', :branch => 'main', :modular_headers => true
pod 'FFmpegKit', :git => 'https://github.com/kingslay/FFmpegKit.git', :branch => 'main', :modular_headers => true
pod 'Libass', :git => 'https://github.com/kingslay/FFmpegKit.git', :branch => 'main'
post_install do |installer|
react_native_post_install(
installer,
@ -62,5 +56,106 @@ target 'Nuvio' do
:mac_catalyst_enabled => false,
:ccache_enabled => ccache_enabled?(podfile_properties),
)
# Patch RCTThirdPartyComponentsProvider.mm to filter out nil component classes (tvOS compatibility)
# Some third-party libraries don't have Fabric components on tvOS, causing crashes
provider_file = "#{Pod::Config.instance.installation_root}/build/generated/ios/RCTThirdPartyComponentsProvider.mm"
if File.exist?(provider_file)
puts "Patching RCTThirdPartyComponentsProvider.mm for tvOS compatibility..."
content = File.read(provider_file)
# Replace the dictionary literal with a mutable dictionary approach that filters nil values
patched_content = content.gsub(
/\+ \(NSDictionary<NSString \*, Class<RCTComponentViewProtocol>>\s*\*\)thirdPartyFabricComponents\s*\{.*?return thirdPartyComponents;\s*\}/m,
<<~OBJC
+ (NSDictionary<NSString *, Class<RCTComponentViewProtocol>> *)thirdPartyFabricComponents
{
static NSDictionary<NSString *, Class<RCTComponentViewProtocol>> *thirdPartyComponents = nil;
static dispatch_once_t nativeComponentsToken;
dispatch_once(&nativeComponentsToken, ^{
NSMutableDictionary<NSString *, Class<RCTComponentViewProtocol>> *components = [NSMutableDictionary new];
// Helper macro to safely add components (skips nil classes for tvOS compatibility)
#define ADD_COMPONENT(name, className) \\
do { \\
Class cls = NSClassFromString(className); \\
if (cls != nil) { \\
components[name] = cls; \\
} \\
} while(0)
ADD_COMPONENT(@"FastImageView", @"FFFastImageViewComponentView");
ADD_COMPONENT(@"RNCSlider", @"RNCSliderComponentView");
ADD_COMPONENT(@"RNCPicker", @"RNCPickerComponentView");
ADD_COMPONENT(@"LottieAnimationView", @"LottieAnimationViewComponentView");
ADD_COMPONENT(@"RNCTabView", @"RCTTabViewComponentView");
ADD_COMPONENT(@"BottomAccessoryView", @"RCTBottomAccessoryComponentView");
ADD_COMPONENT(@"RNGestureHandlerButton", @"RNGestureHandlerButtonComponentView");
ADD_COMPONENT(@"RNCSafeAreaProvider", @"RNCSafeAreaProviderComponentView");
ADD_COMPONENT(@"RNCSafeAreaView", @"RNCSafeAreaViewComponentView");
ADD_COMPONENT(@"RNSVGCircle", @"RNSVGCircle");
ADD_COMPONENT(@"RNSVGClipPath", @"RNSVGClipPath");
ADD_COMPONENT(@"RNSVGDefs", @"RNSVGDefs");
ADD_COMPONENT(@"RNSVGEllipse", @"RNSVGEllipse");
ADD_COMPONENT(@"RNSVGFeBlend", @"RNSVGFeBlend");
ADD_COMPONENT(@"RNSVGFeColorMatrix", @"RNSVGFeColorMatrix");
ADD_COMPONENT(@"RNSVGFeComposite", @"RNSVGFeComposite");
ADD_COMPONENT(@"RNSVGFeFlood", @"RNSVGFeFlood");
ADD_COMPONENT(@"RNSVGFeGaussianBlur", @"RNSVGFeGaussianBlur");
ADD_COMPONENT(@"RNSVGFeMerge", @"RNSVGFeMerge");
ADD_COMPONENT(@"RNSVGFeOffset", @"RNSVGFeOffset");
ADD_COMPONENT(@"RNSVGFilter", @"RNSVGFilter");
ADD_COMPONENT(@"RNSVGForeignObject", @"RNSVGForeignObject");
ADD_COMPONENT(@"RNSVGGroup", @"RNSVGGroup");
ADD_COMPONENT(@"RNSVGImage", @"RNSVGImage");
ADD_COMPONENT(@"RNSVGLine", @"RNSVGLine");
ADD_COMPONENT(@"RNSVGLinearGradient", @"RNSVGLinearGradient");
ADD_COMPONENT(@"RNSVGMarker", @"RNSVGMarker");
ADD_COMPONENT(@"RNSVGMask", @"RNSVGMask");
ADD_COMPONENT(@"RNSVGPath", @"RNSVGPath");
ADD_COMPONENT(@"RNSVGPattern", @"RNSVGPattern");
ADD_COMPONENT(@"RNSVGRadialGradient", @"RNSVGRadialGradient");
ADD_COMPONENT(@"RNSVGRect", @"RNSVGRect");
ADD_COMPONENT(@"RNSVGSvgView", @"RNSVGSvgView");
ADD_COMPONENT(@"RNSVGSymbol", @"RNSVGSymbol");
ADD_COMPONENT(@"RNSVGTSpan", @"RNSVGTSpan");
ADD_COMPONENT(@"RNSVGText", @"RNSVGText");
ADD_COMPONENT(@"RNSVGTextPath", @"RNSVGTextPath");
ADD_COMPONENT(@"RNSVGUse", @"RNSVGUse");
ADD_COMPONENT(@"RNSFullWindowOverlay", @"RNSFullWindowOverlay");
ADD_COMPONENT(@"RNSModalScreen", @"RNSModalScreen");
ADD_COMPONENT(@"RNSScreenContainer", @"RNSScreenContainerView");
ADD_COMPONENT(@"RNSScreenContentWrapper", @"RNSScreenContentWrapper");
ADD_COMPONENT(@"RNSScreenFooter", @"RNSScreenFooter");
ADD_COMPONENT(@"RNSScreen", @"RNSScreenView");
ADD_COMPONENT(@"RNSScreenNavigationContainer", @"RNSScreenNavigationContainerView");
ADD_COMPONENT(@"RNSScreenStackHeaderConfig", @"RNSScreenStackHeaderConfig");
ADD_COMPONENT(@"RNSScreenStackHeaderSubview", @"RNSScreenStackHeaderSubview");
ADD_COMPONENT(@"RNSScreenStack", @"RNSScreenStackView");
ADD_COMPONENT(@"RNSSearchBar", @"RNSSearchBar");
ADD_COMPONENT(@"RNSStackScreen", @"RNSStackScreenComponentView");
ADD_COMPONENT(@"RNSScreenStackHost", @"RNSScreenStackHostComponentView");
ADD_COMPONENT(@"RNSBottomTabsScreen", @"RNSBottomTabsScreenComponentView");
ADD_COMPONENT(@"RNSBottomTabs", @"RNSBottomTabsHostComponentView");
ADD_COMPONENT(@"RNSBottomTabsAccessory", @"RNSBottomTabsAccessoryComponentView");
ADD_COMPONENT(@"RNSBottomTabsAccessoryContent", @"RNSBottomTabsAccessoryContentComponentView");
ADD_COMPONENT(@"RNSSplitViewHost", @"RNSSplitViewHostComponentView");
ADD_COMPONENT(@"RNSSplitViewScreen", @"RNSSplitViewScreenComponentView");
ADD_COMPONENT(@"RNSSafeAreaView", @"RNSSafeAreaViewComponentView");
#undef ADD_COMPONENT
thirdPartyComponents = [components copy];
});
return thirdPartyComponents;
}
OBJC
)
File.write(provider_file, patched_content)
puts "Patched RCTThirdPartyComponentsProvider.mm successfully!"
end
end
end

File diff suppressed because it is too large Load diff

889
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -67,7 +67,7 @@
"lottie-react-native": "~7.3.1",
"posthog-react-native": "^4.4.0",
"react": "19.1.0",
"react-native": "0.81.4",
"react-native": "npm:react-native-tvos@0.81-stable",
"react-native-boost": "^0.6.2",
"react-native-bottom-tabs": "^1.0.2",
"react-native-gesture-handler": "^2.29.1",
@ -93,6 +93,7 @@
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@react-native-tvos/config-tv": "^0.1.4",
"@types/crypto-js": "^4.2.2",
"@types/react": "~18.3.12",
"@types/react-native": "^0.72.8",
@ -103,5 +104,12 @@
"typescript": "^5.3.3",
"xcode": "^3.0.1"
},
"expo": {
"install": {
"exclude": [
"react-native"
]
}
},
"private": true
}

View file

@ -0,0 +1,87 @@
diff --git a/node_modules/react-native-bottom-tabs/ios/BottomAccessoryProvider.swift b/node_modules/react-native-bottom-tabs/ios/BottomAccessoryProvider.swift
index 539efee..dc3f2fd 100644
--- a/node_modules/react-native-bottom-tabs/ios/BottomAccessoryProvider.swift
+++ b/node_modules/react-native-bottom-tabs/ios/BottomAccessoryProvider.swift
@@ -8,8 +8,8 @@ import SwiftUI
self.delegate = delegate
}
- #if !os(macOS)
- @available(iOS 26.0, *)
+ #if !os(macOS) && !os(tvOS)
+ @available(iOS 26.0, visionOS 3.0, *)
public func emitPlacementChanged(_ placement: TabViewBottomAccessoryPlacement?) {
var placementValue = "none"
if placement == .inline {
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 d699315..a78689a 100644
--- a/node_modules/react-native-bottom-tabs/ios/TabView/NewTabView.swift
+++ b/node_modules/react-native-bottom-tabs/ios/TabView/NewTabView.swift
@@ -67,11 +67,11 @@ struct ConditionalBottomAccessoryModifier: ViewModifier {
}
func body(content: Content) -> some View {
- #if os(macOS)
- // tabViewBottomAccessory is not available on macOS
+ #if os(macOS) || os(tvOS)
+ // tabViewBottomAccessory is not available on macOS or tvOS
content
#else
- if #available(iOS 26.0, tvOS 26.0, visionOS 3.0, *), bottomAccessoryView != nil {
+ if #available(iOS 26.0, visionOS 3.0, *), bottomAccessoryView != nil {
content
.tabViewBottomAccessory {
renderBottomAccessoryView()
@@ -84,9 +84,9 @@ struct ConditionalBottomAccessoryModifier: ViewModifier {
@ViewBuilder
private func renderBottomAccessoryView() -> some View {
- #if !os(macOS)
+ #if !os(macOS) && !os(tvOS)
if let bottomAccessoryView {
- if #available(iOS 26.0, *) {
+ if #available(iOS 26.0, visionOS 3.0, *) {
BottomAccessoryRepresentableView(view: bottomAccessoryView)
}
}
@@ -94,8 +94,8 @@ struct ConditionalBottomAccessoryModifier: ViewModifier {
}
}
-#if !os(macOS)
-@available(iOS 26.0, *)
+#if !os(macOS) && !os(tvOS)
+@available(iOS 26.0, visionOS 3.0, *)
struct BottomAccessoryRepresentableView: PlatformViewRepresentable {
@Environment(\.tabViewBottomAccessoryPlacement) var tabViewBottomAccessoryPlacement
var view: PlatformView
diff --git a/node_modules/react-native-bottom-tabs/ios/TabViewImpl.swift b/node_modules/react-native-bottom-tabs/ios/TabViewImpl.swift
index 72938be..f8325bb 100644
--- a/node_modules/react-native-bottom-tabs/ios/TabViewImpl.swift
+++ b/node_modules/react-native-bottom-tabs/ios/TabViewImpl.swift
@@ -281,8 +281,8 @@ extension View {
@ViewBuilder
func tabBarMinimizeBehavior(_ behavior: MinimizeBehavior?) -> some View {
- #if compiler(>=6.2)
- if #available(iOS 26.0, macOS 26.0, *) {
+ #if compiler(>=6.2) && !os(tvOS)
+ if #available(iOS 26.0, macOS 26.0, visionOS 3.0, *) {
if let behavior {
self.tabBarMinimizeBehavior(behavior.convert())
} else {
diff --git a/node_modules/react-native-bottom-tabs/ios/TabViewProps.swift b/node_modules/react-native-bottom-tabs/ios/TabViewProps.swift
index cd098c0..4f90598 100644
--- a/node_modules/react-native-bottom-tabs/ios/TabViewProps.swift
+++ b/node_modules/react-native-bottom-tabs/ios/TabViewProps.swift
@@ -6,8 +6,8 @@ internal enum MinimizeBehavior: String {
case onScrollUp
case onScrollDown
-#if compiler(>=6.2)
- @available(iOS 26.0, macOS 26.0, *)
+#if compiler(>=6.2) && !os(tvOS)
+ @available(iOS 26.0, macOS 26.0, visionOS 3.0, *)
func convert() -> TabBarMinimizeBehavior {
#if os(macOS)
return .automatic

View file

@ -1,13 +1,28 @@
import { useEffect, useRef } from 'react';
import { StatusBar, Platform, Dimensions, AppState } from 'react-native';
import RNImmersiveMode from 'react-native-immersive-mode';
import * as NavigationBar from 'expo-navigation-bar';
import * as Brightness from 'expo-brightness';
import { activateKeepAwakeAsync, deactivateKeepAwake } from 'expo-keep-awake';
import { logger } from '../../../../utils/logger';
import { useFocusEffect } from '@react-navigation/native';
import { useCallback } from 'react';
// Check if running on TV
const isTV = Platform.isTV;
// Conditionally import modules not available on Android TV
let RNImmersiveMode: any = null;
let NavigationBar: typeof import('expo-navigation-bar') | null = null;
let Brightness: typeof import('expo-brightness') | null = null;
if (!isTV) {
try {
RNImmersiveMode = require('react-native-immersive-mode').default;
NavigationBar = require('expo-navigation-bar');
Brightness = require('expo-brightness');
} catch (e) {
logger.warn('[usePlayerSetup] Some player modules not available:', e);
}
}
const DEBUG_MODE = false;
export const usePlayerSetup = (
@ -34,32 +49,40 @@ export const usePlayerSetup = (
}, [paused]);
const enableImmersiveMode = async () => {
if (Platform.OS === 'android') {
// Standard immersive mode
RNImmersiveMode.setBarTranslucent(true);
RNImmersiveMode.fullLayout(true);
if (Platform.OS === 'android' && !isTV) {
// Standard immersive mode (not available on TV)
if (RNImmersiveMode) {
RNImmersiveMode.setBarTranslucent(true);
RNImmersiveMode.fullLayout(true);
}
StatusBar.setHidden(true, 'none');
// Explicitly hide bottom navigation bar using Expo
try {
await NavigationBar.setVisibilityAsync("hidden");
await NavigationBar.setBehaviorAsync("overlay-swipe");
} catch (e) {
// Ignore errors on non-supported devices
if (NavigationBar) {
try {
await NavigationBar.setVisibilityAsync("hidden");
await NavigationBar.setBehaviorAsync("overlay-swipe");
} catch (e) {
// Ignore errors on non-supported devices
}
}
}
};
const disableImmersiveMode = async () => {
if (Platform.OS === 'android') {
RNImmersiveMode.setBarTranslucent(false);
RNImmersiveMode.fullLayout(false);
if (Platform.OS === 'android' && !isTV) {
if (RNImmersiveMode) {
RNImmersiveMode.setBarTranslucent(false);
RNImmersiveMode.fullLayout(false);
}
StatusBar.setHidden(false, 'fade');
try {
await NavigationBar.setVisibilityAsync("visible");
} catch (e) {
// Ignore
if (NavigationBar) {
try {
await NavigationBar.setVisibilityAsync("visible");
} catch (e) {
// Ignore
}
}
}
};
@ -84,10 +107,14 @@ export const usePlayerSetup = (
// Initialize volume (default to 1.0)
setVolume(1.0);
// Initialize Brightness
// Initialize Brightness (skip on TV)
const initBrightness = async () => {
if (!Brightness) {
setBrightness(1.0);
return;
}
try {
if (Platform.OS === 'android') {
if (Platform.OS === 'android' && !isTV) {
try {
const [sysBright, sysMode] = await Promise.all([
(Brightness as any).getSystemBrightnessAsync?.(),

View file

@ -1,11 +1,25 @@
import { useEffect, useRef, useCallback } from 'react';
import { StatusBar, Dimensions, AppState, InteractionManager, Platform } from 'react-native';
import * as Brightness from 'expo-brightness';
import * as ScreenOrientation from 'expo-screen-orientation';
import { activateKeepAwakeAsync, deactivateKeepAwake } from 'expo-keep-awake';
import { logger } from '../../../utils/logger';
import { useFocusEffect } from '@react-navigation/native';
// Check if running on TV
const isTV = Platform.isTV;
// Conditionally import Brightness and ScreenOrientation (not available on TV)
let Brightness: typeof import('expo-brightness') | null = null;
let ScreenOrientation: typeof import('expo-screen-orientation') | null = null;
if (!isTV) {
try {
Brightness = require('expo-brightness');
ScreenOrientation = require('expo-screen-orientation');
} catch (e) {
logger.warn('[usePlayerSetup] Brightness/ScreenOrientation not available:', e);
}
}
interface PlayerSetupConfig {
setScreenDimensions: (dim: any) => void;
setVolume: (vol: number) => void;
@ -72,11 +86,15 @@ export const usePlayerSetup = (config: PlayerSetupConfig) => {
// Initialize volume (normalized 0-1 for cross-platform)
setVolume(1.0);
// Initialize Brightness
// Initialize Brightness (skip on TV)
const initBrightness = () => {
if (!Brightness) {
setBrightness(1.0);
return;
}
InteractionManager.runAfterInteractions(async () => {
try {
const currentBrightness = await Brightness.getBrightnessAsync();
const currentBrightness = await Brightness!.getBrightnessAsync();
setBrightness(currentBrightness);
} catch (error) {
logger.warn('[usePlayerSetup] Error getting initial brightness:', error);
@ -95,9 +113,12 @@ export const usePlayerSetup = (config: PlayerSetupConfig) => {
const orientationLocked = useRef(false);
useEffect(() => {
// Skip orientation lock on TV (not needed)
if (!ScreenOrientation) return;
if (isOpeningAnimationComplete && !orientationLocked.current) {
const task = InteractionManager.runAfterInteractions(() => {
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE)
ScreenOrientation!.lockAsync(ScreenOrientation!.OrientationLock.LANDSCAPE)
.then(() => {
orientationLocked.current = true;
})
@ -109,8 +130,11 @@ export const usePlayerSetup = (config: PlayerSetupConfig) => {
useEffect(() => {
return () => {
// Skip on TV
if (!ScreenOrientation) return;
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.DEFAULT)
.then(() => ScreenOrientation.unlockAsync())
.then(() => ScreenOrientation!.unlockAsync())
.catch(() => { });
};
}, []);

View file

@ -1,6 +1,5 @@
import React from 'react';
import * as ExpoClipboard from 'expo-clipboard';
import { View, Text, TouchableOpacity, StyleSheet, useWindowDimensions } from 'react-native';
import { View, Text, TouchableOpacity, StyleSheet, useWindowDimensions, Platform } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import Animated, {
FadeIn,
@ -9,6 +8,19 @@ import Animated, {
ZoomOut,
} from 'react-native-reanimated';
// Check if running on TV platform
const isTV = Platform.isTV;
// Conditionally import expo-clipboard (not available on TV)
let ExpoClipboard: typeof import('expo-clipboard') | null = null;
if (!isTV) {
try {
ExpoClipboard = require('expo-clipboard');
} catch (e) {
// Silently fail - copy functionality won't be available
}
}
interface ErrorModalProps {
showErrorModal: boolean;
setShowErrorModal: (show: boolean) => void;
@ -34,6 +46,9 @@ export const ErrorModal: React.FC<ErrorModalProps> = ({
};
const handleCopy = async () => {
// Skip on TV or if clipboard is not available
if (!ExpoClipboard) return;
await ExpoClipboard.setStringAsync(errorDetails);
setCopied(true);
setTimeout(() => setCopied(false), 2000);

View file

@ -1,6 +1,20 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { getColors } from 'react-native-image-colors';
import type { ImageColorsResult } from 'react-native-image-colors';
import { Platform } from 'react-native';
// Check if running on TV platform
const isTV = Platform.isTV;
// Conditionally import react-native-image-colors (not available on TV)
let getColors: typeof import('react-native-image-colors').getColors | null = null;
type ImageColorsResult = import('react-native-image-colors').ImageColorsResult;
if (!isTV) {
try {
getColors = require('react-native-image-colors').getColors;
} catch (e) {
// Silently fail - will use fallback colors
}
}
interface DominantColorResult {
dominantColor: string | null;
@ -16,11 +30,11 @@ const calculateVibrancy = (hex: string): number => {
const r = parseInt(hex.substr(1, 2), 16);
const g = parseInt(hex.substr(3, 2), 16);
const b = parseInt(hex.substr(5, 2), 16);
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const saturation = max === 0 ? 0 : (max - min) / max;
return saturation * (max / 255);
};
@ -29,7 +43,7 @@ const calculateBrightness = (hex: string): number => {
const r = parseInt(hex.substr(1, 2), 16);
const g = parseInt(hex.substr(3, 2), 16);
const b = parseInt(hex.substr(5, 2), 16);
return (r * 299 + g * 587 + b * 114) / 1000;
};
@ -38,18 +52,18 @@ const darkenColor = (hex: string, factor: number = 0.1): string => {
const r = parseInt(hex.substr(1, 2), 16);
const g = parseInt(hex.substr(3, 2), 16);
const b = parseInt(hex.substr(5, 2), 16);
const newR = Math.floor(r * factor);
const newG = Math.floor(g * factor);
const newB = Math.floor(b * factor);
return `#${newR.toString(16).padStart(2, '0')}${newG.toString(16).padStart(2, '0')}${newB.toString(16).padStart(2, '0')}`;
};
// Enhanced color selection logic
const selectBestColor = (result: ImageColorsResult): string => {
let candidates: string[] = [];
if (result.platform === 'android') {
// Collect all available colors
candidates = [
@ -80,22 +94,22 @@ const selectBestColor = (result: ImageColorsResult): string => {
result.lightMuted
].filter(Boolean);
}
if (candidates.length === 0) {
return '#1a1a1a';
}
// Score each color based on vibrancy and appropriateness for backgrounds
const scoredColors = candidates.map(color => {
const brightness = calculateBrightness(color);
const vibrancy = calculateVibrancy(color);
// Prefer colors that are:
// 1. Not too bright (good for backgrounds)
// 2. Have decent vibrancy (not too gray)
// 3. Not too dark (still visible)
let score = 0;
// Brightness scoring (prefer medium-dark colors)
if (brightness >= 30 && brightness <= 120) {
score += 3;
@ -104,7 +118,7 @@ const selectBestColor = (result: ImageColorsResult): string => {
} else if (brightness >= 5) {
score += 1;
}
// Vibrancy scoring (prefer some color over pure gray)
if (vibrancy >= 0.3) {
score += 3;
@ -113,17 +127,17 @@ const selectBestColor = (result: ImageColorsResult): string => {
} else if (vibrancy >= 0.05) {
score += 1;
}
return { color, score, brightness, vibrancy };
});
// Sort by score (highest first)
scoredColors.sort((a, b) => b.score - a.score);
// Get the best color
let bestColor = scoredColors[0].color;
const bestBrightness = scoredColors[0].brightness;
// Apply more aggressive darkening to make colors darker overall
if (bestBrightness > 60) {
bestColor = darkenColor(bestColor, 0.18);
@ -134,16 +148,17 @@ const selectBestColor = (result: ImageColorsResult): string => {
} else {
bestColor = darkenColor(bestColor, 0.7);
}
return bestColor;
};
// Preload function to start extraction early
export const preloadDominantColor = async (imageUri: string | null) => {
if (!imageUri || colorCache.has(imageUri)) return;
// Skip on TV or if getColors is not available
if (!getColors || !imageUri || colorCache.has(imageUri)) return;
if (__DEV__) console.log('[useDominantColor] Preloading color for URI:', imageUri);
try {
// Use highest quality for best color accuracy
const result = await getColors(imageUri, {
@ -201,6 +216,15 @@ export const useDominantColor = (imageUri: string | null): DominantColorResult =
setLoading(true);
setError(null);
// Skip color extraction on TV or if getColors is not available
if (!getColors) {
const fallbackColor = '#1a1a1a';
colorCache.set(uri, fallbackColor);
safelySetColor(fallbackColor);
setLoading(false);
return;
}
// Use highest quality for best color accuracy
const fastResult: ImageColorsResult = await getColors(uri, {
fallback: '#1a1a1a',

View file

@ -1,7 +1,19 @@
import { useRef, useState } from 'react';
import { Animated, Platform } from 'react-native';
import { PanGestureHandlerGestureEvent, State } from 'react-native-gesture-handler';
import * as Brightness from 'expo-brightness';
// Check if running on TV platform
const isTV = Platform.isTV;
// Conditionally import expo-brightness (not available on TV)
let Brightness: typeof import('expo-brightness') | null = null;
if (!isTV) {
try {
Brightness = require('expo-brightness');
} catch (e) {
// Silently fail - brightness control won't be available
}
}
interface GestureControlConfig {
volume: number;
@ -19,67 +31,67 @@ export const usePlayerGestureControls = (config: GestureControlConfig) => {
// State for overlays
const [showVolumeOverlay, setShowVolumeOverlay] = useState(false);
const [showBrightnessOverlay, setShowBrightnessOverlay] = useState(false);
// Animated values
const volumeGestureTranslateY = useRef(new Animated.Value(0)).current;
const brightnessGestureTranslateY = useRef(new Animated.Value(0)).current;
const volumeOverlayOpacity = useRef(new Animated.Value(0)).current;
const brightnessOverlayOpacity = useRef(new Animated.Value(0)).current;
// Tracking refs
const lastVolumeGestureY = useRef(0);
const lastBrightnessGestureY = useRef(0);
const volumeOverlayTimeout = useRef<NodeJS.Timeout | null>(null);
const brightnessOverlayTimeout = useRef<NodeJS.Timeout | null>(null);
// Extract config with defaults and platform adjustments
const volumeRange = config.volumeRange || { min: 0, max: 1 };
const baseVolumeSensitivity = config.volumeSensitivity || 0.006;
const baseBrightnessSensitivity = config.brightnessSensitivity || 0.004;
const overlayTimeout = config.overlayTimeout || 1500;
// Platform-specific sensitivity adjustments
// Android needs higher sensitivity due to different touch handling
const platformMultiplier = Platform.OS === 'android' ? 1.6 : 1.0;
const volumeSensitivity = baseVolumeSensitivity * platformMultiplier;
const brightnessSensitivity = baseBrightnessSensitivity * platformMultiplier;
// Volume gesture handler
const onVolumeGestureEvent = Animated.event(
[{ nativeEvent: { translationY: volumeGestureTranslateY } }],
{
{
useNativeDriver: false,
listener: (event: PanGestureHandlerGestureEvent) => {
const { translationY, state } = event.nativeEvent;
if (state === State.ACTIVE) {
// Auto-initialize on first active frame
if (Math.abs(translationY) < 5 && Math.abs(lastVolumeGestureY.current - translationY) > 20) {
lastVolumeGestureY.current = translationY;
return;
}
// Calculate delta from last position
const deltaY = -(translationY - lastVolumeGestureY.current);
lastVolumeGestureY.current = translationY;
// Normalize sensitivity based on volume range
const rangeMultiplier = volumeRange.max - volumeRange.min;
const volumeChange = deltaY * volumeSensitivity * rangeMultiplier;
const newVolume = Math.max(volumeRange.min, Math.min(volumeRange.max, config.volume + volumeChange));
config.setVolume(newVolume);
if (config.debugMode) {
console.log(`[GestureControls] Volume set to: ${newVolume} (Platform: ${Platform.OS}, Sensitivity: ${volumeSensitivity})`);
}
// Show overlay
if (!showVolumeOverlay) {
setShowVolumeOverlay(true);
volumeOverlayOpacity.setValue(1);
}
// Reset hide timer
if (volumeOverlayTimeout.current) {
clearTimeout(volumeOverlayTimeout.current);
@ -95,40 +107,41 @@ export const usePlayerGestureControls = (config: GestureControlConfig) => {
}
}
);
// Brightness gesture handler
const onBrightnessGestureEvent = Animated.event(
[{ nativeEvent: { translationY: brightnessGestureTranslateY } }],
{
{
useNativeDriver: false,
listener: (event: PanGestureHandlerGestureEvent) => {
const { translationY, state } = event.nativeEvent;
if (state === State.ACTIVE) {
// Auto-initialize
if (Math.abs(translationY) < 5 && Math.abs(lastBrightnessGestureY.current - translationY) > 20) {
lastBrightnessGestureY.current = translationY;
return;
}
const deltaY = -(translationY - lastBrightnessGestureY.current);
lastBrightnessGestureY.current = translationY;
const brightnessChange = deltaY * brightnessSensitivity;
const newBrightness = Math.max(0, Math.min(1, config.brightness + brightnessChange));
config.setBrightness(newBrightness);
Brightness.setBrightnessAsync(newBrightness).catch(() => {});
// Only set device brightness if available (not on TV)
Brightness?.setBrightnessAsync(newBrightness).catch(() => { });
if (config.debugMode) {
console.log(`[GestureControls] Device brightness set to: ${newBrightness} (Platform: ${Platform.OS}, Sensitivity: ${brightnessSensitivity})`);
}
if (!showBrightnessOverlay) {
setShowBrightnessOverlay(true);
brightnessOverlayOpacity.setValue(1);
}
if (brightnessOverlayTimeout.current) {
clearTimeout(brightnessOverlayTimeout.current);
}
@ -143,7 +156,7 @@ export const usePlayerGestureControls = (config: GestureControlConfig) => {
}
}
);
// Cleanup function
const cleanup = () => {
if (volumeOverlayTimeout.current) {
@ -153,18 +166,18 @@ export const usePlayerGestureControls = (config: GestureControlConfig) => {
clearTimeout(brightnessOverlayTimeout.current);
}
};
return {
// Gesture handlers
onVolumeGestureEvent,
onBrightnessGestureEvent,
// Overlay state
showVolumeOverlay,
showBrightnessOverlay,
volumeOverlayOpacity,
brightnessOverlayOpacity,
// Cleanup
cleanup,
};

View file

@ -58,9 +58,21 @@ import homeStyles, { sharedStyles } from '../styles/homeStyles';
import { useTheme } from '../contexts/ThemeContext';
import type { Theme } from '../contexts/ThemeContext';
import { useLoading } from '../contexts/LoadingContext';
import * as ScreenOrientation from 'expo-screen-orientation';
import { mmkvStorage } from '../services/mmkvStorage';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
// Check if running on TV platform
const isTV = Platform.isTV;
// Conditionally import ScreenOrientation (not available on TV)
let ScreenOrientation: typeof import('expo-screen-orientation') | null = null;
if (!isTV) {
try {
ScreenOrientation = require('expo-screen-orientation');
} catch (e) {
// Silently fail
}
}
import { useToast } from '../contexts/ToastContext';
import FirstTimeWelcome from '../components/FirstTimeWelcome';
import { HeaderVisibility } from '../contexts/HeaderVisibility';
@ -445,8 +457,8 @@ const HomeScreen = () => {
statusBarConfig();
// Unlock orientation to allow free rotation
ScreenOrientation.unlockAsync().catch(() => { });
// Unlock orientation to allow free rotation (skip on TV)
ScreenOrientation?.unlockAsync().catch(() => { });
return () => {
// Stop trailer when screen loses focus (navigating to other screens)
@ -537,9 +549,11 @@ const HomeScreen = () => {
// Don't clear cache before player - causes broken images on return
// FastImage's native libraries handle memory efficiently
// Lock orientation to landscape before navigation to prevent glitches
// Lock orientation to landscape before navigation to prevent glitches (skip on TV)
try {
await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE);
if (ScreenOrientation) {
await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE);
}
// Longer delay to ensure orientation is fully set before navigation
await new Promise(resolve => setTimeout(resolve, 200));

View file

@ -29,8 +29,24 @@ import { useTraktContext } from '../contexts/TraktContext';
import { useTheme } from '../contexts/ThemeContext';
import { catalogService } from '../services/catalogService';
import { fetchTotalDownloads } from '../services/githubReleaseService';
import * as WebBrowser from 'expo-web-browser';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
// Check if running on TV
const isTV = Platform.isTV;
// Lazy load WebBrowser to avoid native module errors on TV
let _webBrowser: typeof import('expo-web-browser') | null = null;
const getWebBrowser = () => {
if (_webBrowser !== null) return _webBrowser;
if (isTV) return null;
try {
_webBrowser = require('expo-web-browser');
return _webBrowser;
} catch (e) {
return null;
}
};
import * as Sentry from '@sentry/react-native';
import { getDisplayedAppVersion } from '../utils/version';
import CustomAlert from '../components/CustomAlert';
@ -945,9 +961,17 @@ const SettingsScreen: React.FC = () => {
<View style={styles.discordContainer}>
<TouchableOpacity
style={[styles.discordButton, { backgroundColor: 'transparent', paddingVertical: 0, paddingHorizontal: 0, marginBottom: 8 }]}
onPress={() => WebBrowser.openBrowserAsync('https://ko-fi.com/tapframe', {
presentationStyle: Platform.OS === 'ios' ? WebBrowser.WebBrowserPresentationStyle.FORM_SHEET : WebBrowser.WebBrowserPresentationStyle.FORM_SHEET
})}
onPress={() => {
const url = 'https://ko-fi.com/tapframe';
const wb = getWebBrowser();
if (wb) {
wb.openBrowserAsync(url, {
presentationStyle: Platform.OS === 'ios' ? wb.WebBrowserPresentationStyle.FORM_SHEET : wb.WebBrowserPresentationStyle.FORM_SHEET
});
} else {
Linking.openURL(url);
}
}}
activeOpacity={0.7}
>
<FastImage
@ -1071,9 +1095,17 @@ const SettingsScreen: React.FC = () => {
<View style={styles.discordContainer}>
<TouchableOpacity
style={[styles.discordButton, { backgroundColor: 'transparent', paddingVertical: 0, paddingHorizontal: 0, marginBottom: 8 }]}
onPress={() => WebBrowser.openBrowserAsync('https://ko-fi.com/tapframe', {
presentationStyle: Platform.OS === 'ios' ? WebBrowser.WebBrowserPresentationStyle.FORM_SHEET : WebBrowser.WebBrowserPresentationStyle.FORM_SHEET
})}
onPress={() => {
const url = 'https://ko-fi.com/tapframe';
const wb = getWebBrowser();
if (wb) {
wb.openBrowserAsync(url, {
presentationStyle: Platform.OS === 'ios' ? wb.WebBrowserPresentationStyle.FORM_SHEET : wb.WebBrowserPresentationStyle.FORM_SHEET
});
} else {
Linking.openURL(url);
}
}}
activeOpacity={0.7}
>
<FastImage

View file

@ -24,8 +24,6 @@ import Animated, {
runOnJS
} from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import * as ScreenOrientation from 'expo-screen-orientation';
import { useRoute, useNavigation, useFocusEffect } from '@react-navigation/native';
import { RouteProp } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';

View file

@ -13,7 +13,6 @@ import {
Switch,
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { makeRedirectUri, useAuthRequest, ResponseType, Prompt, CodeChallengeMethod } from 'expo-auth-session';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import FastImage from '@d11/react-native-fast-image';
import { traktService, TraktUser } from '../services/traktService';
@ -26,6 +25,28 @@ import { useTraktAutosyncSettings } from '../hooks/useTraktAutosyncSettings';
import { colors } from '../styles';
import CustomAlert from '../components/CustomAlert';
// Check if running on TV platform
const isTV = Platform.isTV;
// Conditionally import expo-auth-session (not available on TV)
let makeRedirectUri: typeof import('expo-auth-session').makeRedirectUri | null = null;
let useAuthRequest: typeof import('expo-auth-session').useAuthRequest | null = null;
let ResponseType: typeof import('expo-auth-session').ResponseType | null = null;
let CodeChallengeMethod: typeof import('expo-auth-session').CodeChallengeMethod | null = null;
if (!isTV) {
try {
const authSession = require('expo-auth-session');
makeRedirectUri = authSession.makeRedirectUri;
useAuthRequest = authSession.useAuthRequest;
ResponseType = authSession.ResponseType;
CodeChallengeMethod = authSession.CodeChallengeMethod;
} catch (e) {
// Silently fail - auth won't be available on TV
logger.warn('[TraktSettingsScreen] expo-auth-session not available');
}
}
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
// Trakt configuration
@ -39,11 +60,11 @@ const discovery = {
tokenEndpoint: 'https://api.trakt.tv/oauth/token',
};
// For use with deep linking
const redirectUri = makeRedirectUri({
// For use with deep linking (only on non-TV platforms)
const redirectUri = makeRedirectUri?.({
scheme: 'nuvio',
path: 'auth/trakt',
});
}) || 'nuvio://auth/trakt';
const TraktSettingsScreen: React.FC = () => {
const { settings, updateSetting } = useSettings();
@ -53,7 +74,7 @@ const TraktSettingsScreen: React.FC = () => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [userProfile, setUserProfile] = useState<TraktUser | null>(null);
const { currentTheme } = useTheme();
const {
settings: autosyncSettings,
isSyncing,
@ -101,7 +122,7 @@ const TraktSettingsScreen: React.FC = () => {
try {
const authenticated = await traktService.isAuthenticated();
setIsAuthenticated(authenticated);
if (authenticated) {
const profile = await traktService.getUserProfile();
setUserProfile(profile);
@ -119,18 +140,23 @@ const TraktSettingsScreen: React.FC = () => {
checkAuthStatus();
}, [checkAuthStatus]);
// Setup expo-auth-session hook with PKCE
const [request, response, promptAsync] = useAuthRequest(
{
clientId: TRAKT_CLIENT_ID,
scopes: [],
redirectUri: redirectUri,
responseType: ResponseType.Code,
usePKCE: true,
codeChallengeMethod: CodeChallengeMethod.S256,
},
discovery
);
// Setup expo-auth-session hook with PKCE (only on non-TV platforms)
// On TV, we'll return null values and disable auth
const authResult = useAuthRequest && ResponseType && CodeChallengeMethod
? useAuthRequest(
{
clientId: TRAKT_CLIENT_ID,
scopes: [],
redirectUri: redirectUri,
responseType: ResponseType.Code,
usePKCE: true,
codeChallengeMethod: CodeChallengeMethod.S256,
},
discovery
)
: [null, null, async () => ({ type: 'dismiss' as const })];
const [request, response, promptAsync] = authResult as any;
const [isExchangingCode, setIsExchangingCode] = useState(false);
@ -151,8 +177,8 @@ const TraktSettingsScreen: React.FC = () => {
'Successfully Connected',
'Your Trakt account has been connected successfully.',
[
{
label: 'OK',
{
label: 'OK',
onPress: () => navigation.goBack(),
}
]
@ -190,9 +216,9 @@ const TraktSettingsScreen: React.FC = () => {
'Sign Out',
'Are you sure you want to sign out of your Trakt account?',
[
{ label: 'Cancel', onPress: () => {} },
{
label: 'Sign Out',
{ label: 'Cancel', onPress: () => { } },
{
label: 'Sign Out',
onPress: async () => {
setIsLoading(true);
try {
@ -224,26 +250,26 @@ const TraktSettingsScreen: React.FC = () => {
onPress={() => navigation.goBack()}
style={styles.backButton}
>
<MaterialIcons
name="arrow-back"
size={24}
color={isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark}
<MaterialIcons
name="arrow-back"
size={24}
color={isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark}
/>
<Text style={[styles.backText, { color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }]}>
Settings
</Text>
</TouchableOpacity>
<View style={styles.headerActions}>
{/* Empty for now, but ready for future actions */}
</View>
</View>
<Text style={[styles.headerTitle, { color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }]}>
Trakt Settings
</Text>
<ScrollView
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
>
@ -259,8 +285,8 @@ const TraktSettingsScreen: React.FC = () => {
<View style={styles.profileContainer}>
<View style={styles.profileHeader}>
{userProfile.avatar ? (
<FastImage
source={{ uri: userProfile.avatar }}
<FastImage
source={{ uri: userProfile.avatar }}
style={styles.avatar}
resizeMode={FastImage.resizeMode.cover}
/>
@ -315,7 +341,7 @@ const TraktSettingsScreen: React.FC = () => {
</View>
) : (
<View style={styles.signInContainer}>
<TraktIcon
<TraktIcon
width={120}
height={120}
style={styles.traktLogo}
@ -497,7 +523,7 @@ const TraktSettingsScreen: React.FC = () => {
</View>
)}
</ScrollView>
<CustomAlert
visible={alertVisible}
title={alertTitle}

View file

@ -1,4 +1,3 @@
import * as Notifications from 'expo-notifications';
import { Platform, AppState, AppStateStatus } from 'react-native';
import { mmkvStorage } from './mmkvStorage';
import { parseISO, differenceInHours, isToday, addDays, isAfter, startOfToday } from 'date-fns';
@ -9,13 +8,28 @@ import { tmdbService } from './tmdbService';
import { logger } from '../utils/logger';
import { memoryManager } from '../utils/memoryManager';
// Check if running on TV platform (tvOS or Android TV)
const isTV = Platform.isTV;
// Conditionally import expo-notifications only on non-TV platforms
// This prevents the native module error on TV
let Notifications: typeof import('expo-notifications') | null = null;
let SchedulableTriggerInputTypes: any = null;
if (!isTV) {
try {
// Dynamic require to avoid loading on TV
Notifications = require('expo-notifications');
SchedulableTriggerInputTypes = Notifications?.SchedulableTriggerInputTypes;
} catch (e) {
logger.warn('[NotificationService] expo-notifications not available:', e);
}
}
// Define notification storage keys
const NOTIFICATION_STORAGE_KEY = 'stremio-notifications';
const NOTIFICATION_SETTINGS_KEY = 'stremio-notification-settings';
// Import the correct type from Notifications
const { SchedulableTriggerInputTypes } = Notifications;
// Notification settings interface
export interface NotificationSettings {
enabled: boolean;
@ -27,10 +41,10 @@ export interface NotificationSettings {
// Default notification settings
const DEFAULT_NOTIFICATION_SETTINGS: NotificationSettings = {
enabled: true,
newEpisodeNotifications: true,
reminderNotifications: true,
upcomingShowsNotifications: true,
enabled: !isTV, // Disable by default on TV
newEpisodeNotifications: !isTV,
reminderNotifications: !isTV,
upcomingShowsNotifications: !isTV,
timeBeforeAiring: 24, // 24 hours before airing
};
@ -60,6 +74,12 @@ class NotificationService {
private lastDownloadNotificationTime: Map<string, number> = new Map();
private constructor() {
// Skip notification initialization on TV platforms
if (isTV) {
logger.log('[NotificationService] Notifications disabled on TV platform');
return;
}
// Initialize notifications
this.configureNotifications();
this.loadSettings();
@ -77,6 +97,9 @@ class NotificationService {
}
private async configureNotifications() {
// Skip on TV platforms or if Notifications not available
if (isTV || !Notifications) return;
// Configure notification behavior
await Notifications.setNotificationHandler({
handleNotification: async () => ({
@ -152,6 +175,9 @@ class NotificationService {
}
async scheduleEpisodeNotification(item: NotificationItem): Promise<string | null> {
// Skip on TV platforms
if (isTV) return null;
if (!this.settings.enabled || !this.settings.newEpisodeNotifications) {
return null;
}
@ -185,7 +211,7 @@ class NotificationService {
}
// Schedule the notification
const notificationId = await Notifications.scheduleNotificationAsync({
const notificationId = await Notifications!.scheduleNotificationAsync({
content: {
title: `New Episode: ${item.seriesName}`,
body: `S${item.season}:E${item.episode} - ${item.episodeTitle} is airing soon!`,
@ -196,7 +222,7 @@ class NotificationService {
},
trigger: {
date: notificationTime,
type: SchedulableTriggerInputTypes.DATE,
type: SchedulableTriggerInputTypes?.DATE,
},
});

75
src/utils/tvPlatform.ts Normal file
View file

@ -0,0 +1,75 @@
/**
* TV Platform Utilities
*
* This module provides utilities for handling TV-specific behavior
* on tvOS and Android TV platforms.
*/
import { Platform } from 'react-native';
/**
* Check if the app is running on a TV platform (tvOS or Android TV)
*/
export const isTV = Platform.isTV;
/**
* Check if the app is running on tvOS specifically
*/
export const isTVOS = Platform.OS === 'ios' && Platform.isTV;
/**
* Check if the app is running on Android TV specifically
*/
export const isAndroidTV = Platform.OS === 'android' && Platform.isTV;
/**
* Features that are NOT supported on TV platforms
*/
export const unsupportedTVFeatures = {
// Push notifications are not available on TV
pushNotifications: true,
// Haptic feedback doesn't exist on TV
haptics: true,
// Brightness control doesn't make sense on TV
brightnessControl: true,
// Casting from TV doesn't make sense (you're already on TV)
casting: true,
// Device orientation doesn't apply to TV
orientationLock: true,
// Touch gestures need to be adapted for D-pad/remote
touchGestures: true,
} as const;
/**
* Safely execute a function only on non-TV platforms
* Returns undefined if on TV, otherwise returns the function result
*/
export function runIfNotTV<T>(fn: () => T): T | undefined {
if (isTV) return undefined;
return fn();
}
/**
* Safely execute an async function only on non-TV platforms
* Returns undefined if on TV, otherwise returns the awaited result
*/
export async function runIfNotTVAsync<T>(fn: () => Promise<T>): Promise<T | undefined> {
if (isTV) return undefined;
return fn();
}
/**
* Get a TV-safe value, returning the fallback on TV platforms
*/
export function tvSafeValue<T>(value: T, tvFallback: T): T {
return isTV ? tvFallback : value;
}
/**
* Log messages about TV platform limitations (development only)
*/
export function logTVUnsupported(feature: string): void {
if (__DEV__ && isTV) {
console.log(`[TV] Feature "${feature}" is not supported on TV platforms`);
}
}