Compare commits
84 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4daab74e27 | ||
|
|
a7fbd567fd | ||
|
|
f90752bdb7 | ||
|
|
c5590639b1 | ||
|
|
098ab73ba1 | ||
|
|
060b0b927b | ||
|
|
786e06b27f | ||
|
|
ef1c34a9c0 | ||
|
|
b97481f2d9 | ||
|
|
8d74b7e7ce | ||
|
|
635c97b1ad | ||
|
|
673c96c917 | ||
|
|
15fc49d84d | ||
|
|
54cfd194f1 | ||
|
|
be561c6d9f | ||
|
|
dc8c27dfc4 | ||
|
|
ce7f92b540 | ||
|
|
f0271cd395 | ||
|
|
2a4c076854 | ||
|
|
c852c56231 | ||
|
|
614ffc12c0 | ||
|
|
d9b2545cdd | ||
|
|
1ae6b4f108 | ||
|
|
373efa0564 | ||
|
|
6c464abdd4 | ||
|
|
5d5d77ae1b | ||
|
|
b2cfc19e96 | ||
|
|
e40e8bb7c5 | ||
|
|
a3158be2bd | ||
|
|
e305dee777 | ||
|
|
415efd4e03 | ||
|
|
f027788266 | ||
|
|
23acda3167 | ||
|
|
a8b4dc5a01 | ||
|
|
fd4efe6c7f | ||
|
|
68340eac9e | ||
|
|
175d47f71f | ||
|
|
0b764412b2 | ||
|
|
f7c0c670d7 | ||
|
|
18bd6ff3ca | ||
|
|
ac5326ba3f | ||
|
|
c5af56537b | ||
|
|
17cdd503e9 | ||
|
|
688950d0c2 | ||
|
|
eb3082cddb | ||
|
|
32bec08f30 | ||
|
|
a7f850d577 | ||
|
|
08f356cfa4 | ||
|
|
6e975ffe26 | ||
|
|
64981dd110 | ||
|
|
b5156bcc69 | ||
|
|
9ab99a1225 | ||
|
|
d5edec025c | ||
|
|
ef43463b99 | ||
|
|
ca2e95e6f4 | ||
|
|
559c50fa87 | ||
|
|
2ca0a05636 | ||
|
|
363de47313 | ||
|
|
bdb2803371 | ||
|
|
6eae438300 | ||
|
|
707ceb711a | ||
|
|
024646579e | ||
|
|
f895428e3d | ||
|
|
698456c205 | ||
|
|
43cf907a2e | ||
|
|
0a04ba5743 | ||
|
|
8b1a40d2e2 | ||
|
|
51ae0784cf | ||
|
|
efa5d3f629 | ||
|
|
fd5861026d | ||
|
|
1535ef9aac | ||
|
|
bb6f1f32a0 | ||
|
|
3effdee5c0 | ||
|
|
bf15c5fb45 | ||
|
|
5c3ba9e0d8 | ||
|
|
ce0b39d48b | ||
|
|
71e3498876 | ||
|
|
e8ec05bd51 | ||
|
|
2303c32940 | ||
|
|
e9d54bf0d6 | ||
|
|
e435a68aea | ||
|
|
d55143e6fb | ||
|
|
e2719c373d | ||
|
|
b1e9f9b3f8 |
4
.github/FUNDING.yml
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: [tapframe]
|
||||
ko_fi: tapframe
|
||||
1
.gitignore
vendored
|
|
@ -72,3 +72,4 @@ SDK54_UPGRADE_SUMMARY.md
|
|||
SDK54_UPGRADE_SUMMARY.md
|
||||
build-and-publish-app-releases.sh
|
||||
bottomnav.md
|
||||
/TrailerServices
|
||||
|
|
|
|||
1
.vscode/settings.json
vendored
|
|
@ -1,2 +1,3 @@
|
|||
{
|
||||
"java.compile.nullAnalysis.mode": "automatic"
|
||||
}
|
||||
5
App.tsx
|
|
@ -40,6 +40,7 @@ import UpdateService from './src/services/updateService';
|
|||
import { memoryMonitorService } from './src/services/memoryMonitorService';
|
||||
import { aiService } from './src/services/aiService';
|
||||
import { AccountProvider, useAccount } from './src/contexts/AccountContext';
|
||||
import { ToastProvider } from './src/contexts/ToastContext';
|
||||
|
||||
Sentry.init({
|
||||
dsn: 'https://1a58bf436454d346e5852b7bfd3c95e8@o4509536317276160.ingest.de.sentry.io/4509536317734992',
|
||||
|
|
@ -203,7 +204,9 @@ function App(): React.JSX.Element {
|
|||
<TraktProvider>
|
||||
<ThemeProvider>
|
||||
<TrailerProvider>
|
||||
<ThemedApp />
|
||||
<ToastProvider>
|
||||
<ThemedApp />
|
||||
</ToastProvider>
|
||||
</TrailerProvider>
|
||||
</ThemeProvider>
|
||||
</TraktProvider>
|
||||
|
|
|
|||
|
|
@ -94,8 +94,8 @@ android {
|
|||
applicationId 'com.nuvio.app'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 20
|
||||
versionName "1.2.5"
|
||||
versionCode 21
|
||||
versionName "1.2.6"
|
||||
|
||||
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
|
||||
}
|
||||
|
|
@ -117,7 +117,7 @@ android {
|
|||
def abiVersionCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4]
|
||||
applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
def baseVersionCode = 20 // Current versionCode from defaultConfig
|
||||
def baseVersionCode = 21 // Current versionCode 21 from defaultConfig
|
||||
def abiName = output.getFilter(com.android.build.OutputFile.ABI)
|
||||
|
||||
def versionCode = baseVersionCode * 100 // Base multiplier
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2 KiB |
|
Before Width: | Height: | Size: 7 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 7 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
|
|
@ -3,5 +3,5 @@
|
|||
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
|
||||
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
|
||||
<string name="expo_system_ui_user_interface_style" translatable="false">dark</string>
|
||||
<string name="expo_runtime_version">1.2.5</string>
|
||||
<string name="expo_runtime_version">1.2.6</string>
|
||||
</resources>
|
||||
10
app.json
|
|
@ -2,7 +2,7 @@
|
|||
"expo": {
|
||||
"name": "Nuvio",
|
||||
"slug": "nuvio",
|
||||
"version": "1.2.5",
|
||||
"version": "1.2.6",
|
||||
"orientation": "default",
|
||||
"backgroundColor": "#020404",
|
||||
"icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png",
|
||||
|
|
@ -10,14 +10,14 @@
|
|||
"scheme": "nuvio",
|
||||
"newArchEnabled": true,
|
||||
"splash": {
|
||||
"image": "./assets/splash-icon.png",
|
||||
"image": "./src/assets/splash-icon-new.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#020404"
|
||||
},
|
||||
"ios": {
|
||||
"supportsTablet": true,
|
||||
"icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png",
|
||||
"buildNumber": "20",
|
||||
"buildNumber": "21",
|
||||
"infoPlist": {
|
||||
"NSAppTransportSecurity": {
|
||||
"NSAllowsArbitraryLoads": true
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
"WAKE_LOCK"
|
||||
],
|
||||
"package": "com.nuvio.app",
|
||||
"versionCode": 20,
|
||||
"versionCode": 21,
|
||||
"architectures": [
|
||||
"arm64-v8a",
|
||||
"armeabi-v7a",
|
||||
|
|
@ -95,6 +95,6 @@
|
|||
"fallbackToCacheTimeout": 30000,
|
||||
"url": "https://grim-reyna-tapframe-69970143.koyeb.app/api/manifest"
|
||||
},
|
||||
"runtimeVersion": "1.2.5"
|
||||
"runtimeVersion": "1.2.6"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 7 KiB After Width: | Height: | Size: 8 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 85 KiB |
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#151515</color>
|
||||
<color name="ic_launcher_background">#2f2f2f</color>
|
||||
</resources>
|
||||
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 111 KiB |
BIN
assets/icon.png
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 718 B After Width: | Height: | Size: 785 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 5 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 280 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 280 KiB |
|
Before Width: | Height: | Size: 423 KiB After Width: | Height: | Size: 583 KiB |
|
|
@ -4,6 +4,7 @@ module.exports = function (api) {
|
|||
presets: ['babel-preset-expo'],
|
||||
plugins: [
|
||||
'react-native-worklets/plugin',
|
||||
'react-native-boost/plugin',
|
||||
],
|
||||
env: {
|
||||
production: {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@ RCT_EXPORT_VIEW_PROPERTY(paused, BOOL)
|
|||
RCT_EXPORT_VIEW_PROPERTY(volume, 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)
|
||||
|
|
@ -32,11 +37,17 @@ RCT_EXTERN_METHOD(setVolume:(nonnull NSNumber *)node volume:(nonnull NSNumber *)
|
|||
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:(nonnull NSNumber *)nodeTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
|
||||
RCT_EXTERN_METHOD(getAirPlayState:(nonnull NSNumber *)nodeTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
|
||||
RCT_EXTERN_METHOD(showAirPlayPicker:(nonnull NSNumber *)nodeTag)
|
||||
|
||||
@end
|
||||
|
|
|
|||
|
|
@ -34,4 +34,26 @@ class KSPlayerModule: RCTEventEmitter {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func getAirPlayState(_ nodeTag: NSNumber, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
||||
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) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
import Foundation
|
||||
import KSPlayer
|
||||
import React
|
||||
import AVKit
|
||||
|
||||
@objc(KSPlayerView)
|
||||
class KSPlayerView: UIView {
|
||||
|
|
@ -17,6 +18,11 @@ class KSPlayerView: UIView {
|
|||
private var currentVolume: Float = 1.0
|
||||
weak var viewManager: KSPlayerViewManager?
|
||||
|
||||
// Store constraint references for dynamic updates
|
||||
private var subtitleBottomConstraint: NSLayoutConstraint?
|
||||
|
||||
// AirPlay properties (removed duplicate declarations)
|
||||
|
||||
// Event blocks for Fabric
|
||||
@objc var onLoad: RCTDirectEventBlock?
|
||||
@objc var onProgress: RCTDirectEventBlock?
|
||||
|
|
@ -57,15 +63,52 @@ class KSPlayerView: UIView {
|
|||
setTextTrack(textTrack.intValue)
|
||||
}
|
||||
}
|
||||
|
||||
// AirPlay properties
|
||||
@objc var allowsExternalPlayback: Bool = true {
|
||||
didSet {
|
||||
setAllowsExternalPlayback(allowsExternalPlayback)
|
||||
}
|
||||
}
|
||||
|
||||
@objc var usesExternalPlaybackWhileExternalScreenIsActive: Bool = true {
|
||||
didSet {
|
||||
setUsesExternalPlaybackWhileExternalScreenIsActive(usesExternalPlaybackWhileExternalScreenIsActive)
|
||||
}
|
||||
}
|
||||
|
||||
@objc var subtitleBottomOffset: NSNumber = 60 {
|
||||
didSet {
|
||||
print("KSPlayerView: [PROP SETTER] subtitleBottomOffset setter called with value: \(subtitleBottomOffset.floatValue)")
|
||||
updateSubtitlePositioning()
|
||||
}
|
||||
}
|
||||
|
||||
@objc var subtitleFontSize: NSNumber = 16 {
|
||||
didSet {
|
||||
let size = CGFloat(truncating: subtitleFontSize)
|
||||
print("KSPlayerView: [PROP SETTER] subtitleFontSize setter called with value: \(size)")
|
||||
updateSubtitleFont(size: size)
|
||||
}
|
||||
}
|
||||
|
||||
@objc var resizeMode: NSString = "contain" {
|
||||
didSet {
|
||||
print("KSPlayerView: [PROP SETTER] resizeMode setter called with value: \(resizeMode)")
|
||||
applyVideoGravity()
|
||||
}
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setupPlayerView()
|
||||
setupCustomSubtitlePositioning()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setupPlayerView()
|
||||
setupCustomSubtitlePositioning()
|
||||
}
|
||||
|
||||
private func setupPlayerView() {
|
||||
|
|
@ -88,9 +131,113 @@ class KSPlayerView: UIView {
|
|||
playerView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||
])
|
||||
|
||||
// Ensure subtitle views are visible and on top
|
||||
// KSPlayer's subtitleLabel renders internal subtitles
|
||||
playerView.subtitleLabel.isHidden = false
|
||||
playerView.subtitleBackView.isHidden = false
|
||||
// Move subtitle view to main container for independence from video transformations
|
||||
playerView.subtitleBackView.removeFromSuperview()
|
||||
self.addSubview(playerView.subtitleBackView)
|
||||
self.bringSubviewToFront(playerView.subtitleBackView)
|
||||
print("KSPlayerView: [SETUP] Subtitle views made visible")
|
||||
print("KSPlayerView: [SETUP] subtitleLabel.isHidden: \(playerView.subtitleLabel.isHidden)")
|
||||
print("KSPlayerView: [SETUP] subtitleBackView.isHidden: \(playerView.subtitleBackView.isHidden)")
|
||||
print("KSPlayerView: [SETUP] subtitleLabel.frame: \(playerView.subtitleLabel.frame)")
|
||||
print("KSPlayerView: [SETUP] subtitleBackView.frame: \(playerView.subtitleBackView.frame)")
|
||||
|
||||
// Set up player delegates and callbacks
|
||||
setupPlayerCallbacks()
|
||||
}
|
||||
|
||||
private func setupCustomSubtitlePositioning() {
|
||||
// Wait for the player view to be fully set up before modifying subtitle positioning
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
|
||||
self?.adjustSubtitlePositioning()
|
||||
}
|
||||
}
|
||||
|
||||
private func adjustSubtitlePositioning() {
|
||||
// Remove existing constraints for subtitle positioning
|
||||
playerView.subtitleBackView.removeFromSuperview()
|
||||
// Add subtitle view to main container (self) instead of playerView to make it independent of video transformations
|
||||
self.addSubview(playerView.subtitleBackView)
|
||||
// Ensure subtitles are always on top of video
|
||||
self.bringSubviewToFront(playerView.subtitleBackView)
|
||||
|
||||
// Re-add subtitle label to subtitle back view
|
||||
playerView.subtitleBackView.addSubview(playerView.subtitleLabel)
|
||||
|
||||
// Set up new constraints for better mobile visibility
|
||||
playerView.subtitleBackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
playerView.subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// Store the bottom constraint reference for dynamic updates
|
||||
// Constrain to main container (self) instead of playerView to make subtitles independent of video transformations
|
||||
subtitleBottomConstraint = playerView.subtitleBackView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -CGFloat(subtitleBottomOffset.floatValue))
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
// Position subtitles using dynamic offset from React Native
|
||||
subtitleBottomConstraint!,
|
||||
playerView.subtitleBackView.centerXAnchor.constraint(equalTo: self.centerXAnchor),
|
||||
playerView.subtitleBackView.widthAnchor.constraint(lessThanOrEqualTo: self.widthAnchor, constant: -20),
|
||||
playerView.subtitleBackView.heightAnchor.constraint(lessThanOrEqualToConstant: 100),
|
||||
|
||||
// Subtitle label constraints within the back view
|
||||
playerView.subtitleLabel.leadingAnchor.constraint(equalTo: playerView.subtitleBackView.leadingAnchor, constant: 10),
|
||||
playerView.subtitleLabel.trailingAnchor.constraint(equalTo: playerView.subtitleBackView.trailingAnchor, constant: -10),
|
||||
playerView.subtitleLabel.topAnchor.constraint(equalTo: playerView.subtitleBackView.topAnchor, constant: 5),
|
||||
playerView.subtitleLabel.bottomAnchor.constraint(equalTo: playerView.subtitleBackView.bottomAnchor, constant: -5),
|
||||
])
|
||||
|
||||
// Ensure subtitle views are initially hidden
|
||||
playerView.subtitleBackView.isHidden = true
|
||||
playerView.subtitleLabel.isHidden = true
|
||||
|
||||
print("KSPlayerView: Custom subtitle positioning applied - positioned \(subtitleBottomOffset.floatValue)pts from bottom for mobile visibility")
|
||||
}
|
||||
|
||||
private func updateSubtitlePositioning() {
|
||||
// Update subtitle positioning when offset changes
|
||||
print("KSPlayerView: [OFFSET UPDATE] subtitleBottomOffset changed to: \(subtitleBottomOffset.floatValue)")
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
print("KSPlayerView: [OFFSET UPDATE] Applying new positioning with offset: \(self.subtitleBottomOffset.floatValue)")
|
||||
|
||||
// Update the existing constraint instead of recreating everything
|
||||
if let bottomConstraint = self.subtitleBottomConstraint {
|
||||
bottomConstraint.constant = -CGFloat(self.subtitleBottomOffset.floatValue)
|
||||
print("KSPlayerView: [OFFSET UPDATE] Updated constraint constant to: \(bottomConstraint.constant)")
|
||||
} else {
|
||||
// Fallback: recreate positioning if constraint reference is missing
|
||||
print("KSPlayerView: [OFFSET UPDATE] No constraint reference found, recreating positioning")
|
||||
self.adjustSubtitlePositioning()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func applyVideoGravity() {
|
||||
print("KSPlayerView: [VIDEO GRAVITY] Applying resizeMode: \(resizeMode)")
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
let contentMode: UIViewContentMode
|
||||
switch self.resizeMode.lowercased {
|
||||
case "cover":
|
||||
contentMode = .scaleAspectFill
|
||||
case "stretch":
|
||||
contentMode = .scaleToFill
|
||||
case "contain":
|
||||
contentMode = .scaleAspectFit
|
||||
default:
|
||||
contentMode = .scaleAspectFit
|
||||
}
|
||||
|
||||
// Set contentMode on the player itself, not the view
|
||||
self.playerView.playerLayer?.player.contentMode = contentMode
|
||||
print("KSPlayerView: [VIDEO GRAVITY] Set player contentMode to: \(contentMode)")
|
||||
}
|
||||
}
|
||||
|
||||
private func setupPlayerCallbacks() {
|
||||
// Configure KSOptions (use static defaults where required)
|
||||
|
|
@ -103,6 +250,18 @@ class KSPlayerView: UIView {
|
|||
#endif
|
||||
}
|
||||
|
||||
private func updateSubtitleFont(size: CGFloat) {
|
||||
// Update KSPlayer subtitle font size via SubtitleModel
|
||||
SubtitleModel.textFontSize = size
|
||||
// Also directly apply to current label for immediate effect
|
||||
playerView.subtitleLabel.font = SubtitleModel.textFont
|
||||
// Re-render current subtitle parts to apply font
|
||||
if let currentTime = playerView.playerLayer?.player.currentPlaybackTime {
|
||||
_ = playerView.srtControl.subtitle(currentTime: currentTime)
|
||||
}
|
||||
print("KSPlayerView: [FONT UPDATE] Applied subtitle font size: \(size)")
|
||||
}
|
||||
|
||||
func setSource(_ source: NSDictionary) {
|
||||
currentSource = source
|
||||
|
||||
|
|
@ -151,7 +310,15 @@ class KSPlayerView: UIView {
|
|||
playerView.set(resource: resource)
|
||||
|
||||
// Set up delegate after setting the resource
|
||||
playerView.playerLayer?.delegate = self
|
||||
if let playerLayer = playerView.playerLayer {
|
||||
playerLayer.delegate = self
|
||||
print("KSPlayerView: Delegate set successfully on playerLayer")
|
||||
|
||||
// Apply video gravity after player is set up
|
||||
applyVideoGravity()
|
||||
} else {
|
||||
print("KSPlayerView: ERROR - playerLayer is nil, cannot set delegate")
|
||||
}
|
||||
|
||||
// Apply current state
|
||||
if isPaused {
|
||||
|
|
@ -161,6 +328,12 @@ class KSPlayerView: UIView {
|
|||
}
|
||||
|
||||
setVolume(currentVolume)
|
||||
|
||||
// Ensure AirPlay is properly configured after setting source
|
||||
DispatchQueue.main.async {
|
||||
self.setAllowsExternalPlayback(self.allowsExternalPlayback)
|
||||
self.setUsesExternalPlaybackWhileExternalScreenIsActive(self.usesExternalPlaybackWhileExternalScreenIsActive)
|
||||
}
|
||||
}
|
||||
|
||||
private func createOptions(with headers: [String: String]) -> KSOptions {
|
||||
|
|
@ -283,7 +456,7 @@ class KSPlayerView: UIView {
|
|||
print("KSPlayerView: Successfully selected audio track \(trackId)")
|
||||
|
||||
// Verify the selection worked
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
let tracksAfter = player.tracks(mediaType: .audio)
|
||||
for (index, track) in tracksAfter.enumerated() {
|
||||
print("KSPlayerView: After selection - Track \(index) (ID: \(track.trackID)) isEnabled: \(track.isEnabled)")
|
||||
|
|
@ -321,44 +494,110 @@ class KSPlayerView: UIView {
|
|||
}
|
||||
|
||||
func setTextTrack(_ trackId: Int) {
|
||||
if let player = playerView.playerLayer?.player {
|
||||
let textTracks = player.tracks(mediaType: .subtitle)
|
||||
print("KSPlayerView: Available text tracks count: \(textTracks.count)")
|
||||
print("KSPlayerView: Requested text track ID: \(trackId)")
|
||||
|
||||
// First try to find track by trackID (proper way)
|
||||
var selectedTrack: MediaPlayerTrack? = nil
|
||||
var trackIndex: Int = -1
|
||||
|
||||
// Try to find by exact trackID match
|
||||
if let track = textTracks.first(where: { Int($0.trackID) == trackId }) {
|
||||
selectedTrack = track
|
||||
trackIndex = textTracks.firstIndex(where: { $0.trackID == track.trackID }) ?? -1
|
||||
print("KSPlayerView: Found text track by trackID \(trackId) at index \(trackIndex)")
|
||||
}
|
||||
// Fallback: treat trackId as array index
|
||||
else if trackId >= 0 && trackId < textTracks.count {
|
||||
selectedTrack = textTracks[trackId]
|
||||
trackIndex = trackId
|
||||
print("KSPlayerView: Found text track by array index \(trackId) (fallback)")
|
||||
print("KSPlayerView: [SET TEXT TRACK] Starting setTextTrack with trackId: \(trackId)")
|
||||
|
||||
// Wait slightly longer than the 1-second delay for subtitle data source connection
|
||||
// This ensures srtControl.addSubtitle(dataSouce:) has been called in VideoPlayerView
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) { [weak self] in
|
||||
guard let self = self else {
|
||||
print("KSPlayerView: [SET TEXT TRACK] self is nil, aborting")
|
||||
return
|
||||
}
|
||||
|
||||
print("KSPlayerView: [SET TEXT TRACK] Executing delayed track selection")
|
||||
|
||||
if let player = self.playerView.playerLayer?.player {
|
||||
let textTracks = player.tracks(mediaType: .subtitle)
|
||||
print("KSPlayerView: Available text tracks count: \(textTracks.count)")
|
||||
print("KSPlayerView: Requested text track ID: \(trackId)")
|
||||
|
||||
if let track = selectedTrack {
|
||||
print("KSPlayerView: Selecting text track \(trackId) (index: \(trackIndex)): '\(track.name)' (ID: \(track.trackID))")
|
||||
// First try to find track by trackID (proper way)
|
||||
var selectedTrack: MediaPlayerTrack? = nil
|
||||
var trackIndex: Int = -1
|
||||
|
||||
// Use KSPlayer's select method which properly handles track selection
|
||||
player.select(track: track)
|
||||
// Try to find by exact trackID match
|
||||
if let track = textTracks.first(where: { Int($0.trackID) == trackId }) {
|
||||
selectedTrack = track
|
||||
trackIndex = textTracks.firstIndex(where: { $0.trackID == track.trackID }) ?? -1
|
||||
print("KSPlayerView: Found text track by trackID \(trackId) at index \(trackIndex)")
|
||||
}
|
||||
// Fallback: treat trackId as array index
|
||||
else if trackId >= 0 && trackId < textTracks.count {
|
||||
selectedTrack = textTracks[trackId]
|
||||
trackIndex = trackId
|
||||
print("KSPlayerView: Found text track by array index \(trackId) (fallback)")
|
||||
}
|
||||
|
||||
print("KSPlayerView: Successfully selected text track \(trackId)")
|
||||
} else if trackId == -1 {
|
||||
// Disable all subtitles
|
||||
for track in textTracks { track.isEnabled = false }
|
||||
print("KSPlayerView: Disabled all text tracks")
|
||||
if let track = selectedTrack {
|
||||
print("KSPlayerView: Selecting text track \(trackId) (index: \(trackIndex)): '\(track.name)' (ID: \(track.trackID))")
|
||||
|
||||
// First disable all tracks to ensure only one is active
|
||||
for t in textTracks {
|
||||
t.isEnabled = false
|
||||
}
|
||||
|
||||
// Use KSPlayer's select method which properly handles track selection
|
||||
player.select(track: track)
|
||||
|
||||
// Sync srtControl with player track selection
|
||||
// Find the corresponding SubtitleInfo in srtControl and select it
|
||||
if let matchingSubtitleInfo = self.playerView.srtControl.subtitleInfos.first(where: { subtitleInfo in
|
||||
// Try to match by name or track ID
|
||||
subtitleInfo.name.lowercased() == track.name.lowercased() ||
|
||||
subtitleInfo.subtitleID == String(track.trackID)
|
||||
}) {
|
||||
print("KSPlayerView: Found matching SubtitleInfo: \(matchingSubtitleInfo.name) (ID: \(matchingSubtitleInfo.subtitleID))")
|
||||
self.playerView.srtControl.selectedSubtitleInfo = matchingSubtitleInfo
|
||||
print("KSPlayerView: Set srtControl.selectedSubtitleInfo to: \(matchingSubtitleInfo.name)")
|
||||
} else {
|
||||
print("KSPlayerView: No matching SubtitleInfo found for track '\(track.name)' (ID: \(track.trackID))")
|
||||
print("KSPlayerView: Available SubtitleInfos:")
|
||||
for (index, info) in self.playerView.srtControl.subtitleInfos.enumerated() {
|
||||
print("KSPlayerView: [\(index)] name='\(info.name)', subtitleID='\(info.subtitleID)'")
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure subtitle views are visible after selection
|
||||
self.playerView.subtitleLabel.isHidden = false
|
||||
self.playerView.subtitleBackView.isHidden = false
|
||||
|
||||
// Debug: Check the enabled state of all tracks after selection
|
||||
print("KSPlayerView: Track states after selection:")
|
||||
for (index, t) in textTracks.enumerated() {
|
||||
print("KSPlayerView: Track \(index): ID=\(t.trackID), Name='\(t.name)', Enabled=\(t.isEnabled)")
|
||||
}
|
||||
|
||||
// Verify the selection worked after a delay
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
let tracksAfter = player.tracks(mediaType: .subtitle)
|
||||
print("KSPlayerView: Verification after subtitle selection:")
|
||||
for (index, track) in tracksAfter.enumerated() {
|
||||
print("KSPlayerView: Track \(index) (ID: \(track.trackID)) isEnabled: \(track.isEnabled)")
|
||||
}
|
||||
|
||||
// Also verify srtControl selection
|
||||
if let selectedInfo = self.playerView.srtControl.selectedSubtitleInfo {
|
||||
print("KSPlayerView: srtControl.selectedSubtitleInfo: \(selectedInfo.name) (ID: \(selectedInfo.subtitleID))")
|
||||
} else {
|
||||
print("KSPlayerView: srtControl.selectedSubtitleInfo is nil")
|
||||
}
|
||||
}
|
||||
|
||||
print("KSPlayerView: Successfully selected text track \(trackId)")
|
||||
} else if trackId == -1 {
|
||||
// Disable all subtitles
|
||||
for track in textTracks { track.isEnabled = false }
|
||||
// Clear srtControl selection and hide subtitle views
|
||||
self.playerView.srtControl.selectedSubtitleInfo = nil
|
||||
self.playerView.subtitleLabel.isHidden = true
|
||||
self.playerView.subtitleBackView.isHidden = true
|
||||
print("KSPlayerView: Disabled all text tracks and cleared srtControl selection")
|
||||
} else {
|
||||
print("KSPlayerView: Text track \(trackId) not found. Available track IDs: \(textTracks.map { Int($0.trackID) }), array indices: 0..\(textTracks.count - 1)")
|
||||
}
|
||||
} else {
|
||||
print("KSPlayerView: Text track \(trackId) not found. Available track IDs: \(textTracks.map { Int($0.trackID) }), array indices: 0..\(textTracks.count - 1)")
|
||||
print("KSPlayerView: No player available for text track selection")
|
||||
}
|
||||
} else {
|
||||
print("KSPlayerView: No player available for text track selection")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -382,10 +621,27 @@ class KSPlayerView: UIView {
|
|||
}
|
||||
|
||||
let textTracks = player.tracks(mediaType: .subtitle).enumerated().map { index, track in
|
||||
// Create a better display name for subtitles
|
||||
var displayName = track.name
|
||||
if displayName.isEmpty || displayName == "Unknown" {
|
||||
if let language = track.language, !language.isEmpty && language != "Unknown" {
|
||||
displayName = language
|
||||
} else if let languageCode = track.languageCode, !languageCode.isEmpty {
|
||||
displayName = languageCode.uppercased()
|
||||
} else {
|
||||
displayName = "Subtitle \(index + 1)"
|
||||
}
|
||||
}
|
||||
|
||||
// Add language info if not already in the name
|
||||
if let language = track.language, !language.isEmpty && language != "Unknown" && !displayName.lowercased().contains(language.lowercased()) {
|
||||
displayName += " (\(language))"
|
||||
}
|
||||
|
||||
return [
|
||||
"id": Int(track.trackID), // Use actual track ID, not array index
|
||||
"index": index, // Keep index for backward compatibility
|
||||
"name": track.name,
|
||||
"name": displayName,
|
||||
"language": track.language ?? "Unknown",
|
||||
"languageCode": track.languageCode ?? "",
|
||||
"isEnabled": track.isEnabled,
|
||||
|
|
@ -399,6 +655,94 @@ class KSPlayerView: UIView {
|
|||
]
|
||||
}
|
||||
|
||||
// AirPlay methods
|
||||
func setAllowsExternalPlayback(_ allows: Bool) {
|
||||
print("[KSPlayerView] Setting allowsExternalPlayback: \(allows)")
|
||||
playerView.playerLayer?.player.allowsExternalPlayback = allows
|
||||
}
|
||||
|
||||
func setUsesExternalPlaybackWhileExternalScreenIsActive(_ uses: Bool) {
|
||||
print("[KSPlayerView] Setting usesExternalPlaybackWhileExternalScreenIsActive: \(uses)")
|
||||
playerView.playerLayer?.player.usesExternalPlaybackWhileExternalScreenIsActive = uses
|
||||
}
|
||||
|
||||
func showAirPlayPicker() {
|
||||
print("[KSPlayerView] showAirPlayPicker called")
|
||||
|
||||
DispatchQueue.main.async {
|
||||
// Create a temporary route picker view for triggering AirPlay
|
||||
let routePickerView = AVRoutePickerView()
|
||||
routePickerView.tintColor = .white
|
||||
routePickerView.alpha = 0.01 // Nearly invisible but still interactive
|
||||
|
||||
// Find the current view controller
|
||||
guard let viewController = self.findHostViewController() else {
|
||||
print("[KSPlayerView] Could not find view controller for AirPlay picker")
|
||||
return
|
||||
}
|
||||
|
||||
// Add to the view controller's view temporarily
|
||||
viewController.view.addSubview(routePickerView)
|
||||
|
||||
// Position it off-screen but still in the view hierarchy
|
||||
routePickerView.frame = CGRect(x: -100, y: -100, width: 44, height: 44)
|
||||
|
||||
// Force layout
|
||||
viewController.view.setNeedsLayout()
|
||||
viewController.view.layoutIfNeeded()
|
||||
|
||||
// Wait a bit for the view to be ready, then trigger
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
// Find and trigger the AirPlay button
|
||||
self.triggerAirPlayButton(routePickerView)
|
||||
|
||||
// Clean up after a delay
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||
routePickerView.removeFromSuperview()
|
||||
print("[KSPlayerView] Cleaned up temporary AirPlay picker")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func triggerAirPlayButton(_ routePickerView: AVRoutePickerView) {
|
||||
// Recursively find the button in the route picker view
|
||||
func findButton(in view: UIView) -> UIButton? {
|
||||
if let button = view as? UIButton {
|
||||
return button
|
||||
}
|
||||
for subview in view.subviews {
|
||||
if let button = findButton(in: subview) {
|
||||
return button
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if let button = findButton(in: routePickerView) {
|
||||
print("[KSPlayerView] Found AirPlay button, triggering tap")
|
||||
button.sendActions(for: .touchUpInside)
|
||||
} else {
|
||||
print("[KSPlayerView] Could not find AirPlay button in route picker")
|
||||
}
|
||||
}
|
||||
|
||||
func getAirPlayState() -> [String: Any] {
|
||||
guard let player = playerView.playerLayer?.player else {
|
||||
return [
|
||||
"allowsExternalPlayback": false,
|
||||
"usesExternalPlaybackWhileExternalScreenIsActive": false,
|
||||
"isExternalPlaybackActive": false
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
"allowsExternalPlayback": player.allowsExternalPlayback,
|
||||
"usesExternalPlaybackWhileExternalScreenIsActive": player.usesExternalPlaybackWhileExternalScreenIsActive,
|
||||
"isExternalPlaybackActive": player.isExternalPlaybackActive
|
||||
]
|
||||
}
|
||||
|
||||
// Get current player state for React Native
|
||||
func getCurrentState() -> [String: Any] {
|
||||
guard let player = playerView.playerLayer?.player else {
|
||||
|
|
@ -419,6 +763,81 @@ extension KSPlayerView: KSPlayerLayerDelegate {
|
|||
func player(layer: KSPlayerLayer, state: KSPlayerState) {
|
||||
switch state {
|
||||
case .readyToPlay:
|
||||
// Ensure AirPlay is properly configured when player is ready
|
||||
layer.player.allowsExternalPlayback = allowsExternalPlayback
|
||||
layer.player.usesExternalPlaybackWhileExternalScreenIsActive = usesExternalPlaybackWhileExternalScreenIsActive
|
||||
|
||||
// Debug: Check subtitle data source connection
|
||||
let hasSubtitleDataSource = layer.player.subtitleDataSouce != nil
|
||||
print("KSPlayerView: [READY TO PLAY] subtitle data source available: \(hasSubtitleDataSource)")
|
||||
|
||||
// Ensure subtitle views are visible
|
||||
playerView.subtitleLabel.isHidden = false
|
||||
playerView.subtitleBackView.isHidden = false
|
||||
print("KSPlayerView: [READY TO PLAY] Verified subtitle views are visible")
|
||||
print("KSPlayerView: [READY TO PLAY] subtitleLabel.isHidden: \(playerView.subtitleLabel.isHidden)")
|
||||
print("KSPlayerView: [READY TO PLAY] subtitleBackView.isHidden: \(playerView.subtitleBackView.isHidden)")
|
||||
print("KSPlayerView: [READY TO PLAY] subtitleLabel.frame: \(playerView.subtitleLabel.frame)")
|
||||
print("KSPlayerView: [READY TO PLAY] subtitleBackView.frame: \(playerView.subtitleBackView.frame)")
|
||||
|
||||
// Manually connect subtitle data source to srtControl (this is the missing piece!)
|
||||
if let subtitleDataSouce = layer.player.subtitleDataSouce {
|
||||
print("KSPlayerView: [READY TO PLAY] Connecting subtitle data source to srtControl")
|
||||
print("KSPlayerView: [READY TO PLAY] subtitleDataSouce type: \(type(of: subtitleDataSouce))")
|
||||
|
||||
// Check if subtitle data source has any subtitle infos
|
||||
print("KSPlayerView: [READY TO PLAY] subtitleDataSouce has \(subtitleDataSouce.infos.count) subtitle infos")
|
||||
|
||||
for (index, info) in subtitleDataSouce.infos.enumerated() {
|
||||
print("KSPlayerView: [READY TO PLAY] subtitleDataSouce info[\(index)]: ID=\(info.subtitleID), Name='\(info.name)', Enabled=\(info.isEnabled)")
|
||||
}
|
||||
// Wait 1 second like the original KSPlayer code does
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
print("KSPlayerView: [READY TO PLAY] About to add subtitle data source to srtControl")
|
||||
self.playerView.srtControl.addSubtitle(dataSouce: subtitleDataSouce)
|
||||
print("KSPlayerView: [READY TO PLAY] Subtitle data source connected to srtControl")
|
||||
print("KSPlayerView: [READY TO PLAY] srtControl.subtitleInfos.count: \(self.playerView.srtControl.subtitleInfos.count)")
|
||||
|
||||
// Log all subtitle infos
|
||||
for (index, info) in self.playerView.srtControl.subtitleInfos.enumerated() {
|
||||
print("KSPlayerView: [READY TO PLAY] SubtitleInfo[\(index)]: name=\(info.name), isEnabled=\(info.isEnabled), subtitleID=\(info.subtitleID)")
|
||||
}
|
||||
|
||||
// Try to manually trigger subtitle parsing for the current time
|
||||
let currentTime = self.playerView.playerLayer?.player.currentPlaybackTime ?? 0
|
||||
print("KSPlayerView: [READY TO PLAY] Current playback time: \(currentTime)")
|
||||
|
||||
// Force subtitle search for current time
|
||||
let hasSubtitle = self.playerView.srtControl.subtitle(currentTime: currentTime)
|
||||
print("KSPlayerView: [READY TO PLAY] Manual subtitle search result: \(hasSubtitle)")
|
||||
print("KSPlayerView: [READY TO PLAY] Parts count after manual search: \(self.playerView.srtControl.parts.count)")
|
||||
|
||||
if let firstPart = self.playerView.srtControl.parts.first {
|
||||
print("KSPlayerView: [READY TO PLAY] Found subtitle part: start=\(firstPart.start), end=\(firstPart.end), text='\(firstPart.text?.string ?? "nil")'")
|
||||
}
|
||||
|
||||
// Auto-select first enabled subtitle if none selected
|
||||
if self.playerView.srtControl.selectedSubtitleInfo == nil {
|
||||
self.playerView.srtControl.selectedSubtitleInfo = self.playerView.srtControl.subtitleInfos.first { $0.isEnabled }
|
||||
if let selected = self.playerView.srtControl.selectedSubtitleInfo {
|
||||
print("KSPlayerView: [READY TO PLAY] Auto-selected subtitle: \(selected.name)")
|
||||
} else {
|
||||
print("KSPlayerView: [READY TO PLAY] No enabled subtitle found for auto-selection")
|
||||
}
|
||||
} else {
|
||||
print("KSPlayerView: [READY TO PLAY] Subtitle already selected: \(self.playerView.srtControl.selectedSubtitleInfo?.name ?? "unknown")")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print("KSPlayerView: [READY TO PLAY] ERROR: No subtitle data source available")
|
||||
}
|
||||
|
||||
// Determine player backend type
|
||||
let uriString = currentSource?["uri"] as? String
|
||||
let isMKV = uriString?.lowercased().contains(".mkv") ?? false
|
||||
let playerBackend = isMKV ? "KSMEPlayer" : "KSAVPlayer"
|
||||
|
||||
// Send onLoad event to React Native with track information
|
||||
let p = layer.player
|
||||
let tracks = getAvailableTracks()
|
||||
|
|
@ -430,7 +849,8 @@ extension KSPlayerView: KSPlayerLayerDelegate {
|
|||
"height": p.naturalSize.height
|
||||
],
|
||||
"audioTracks": tracks["audioTracks"] ?? [],
|
||||
"textTracks": tracks["textTracks"] ?? []
|
||||
"textTracks": tracks["textTracks"] ?? [],
|
||||
"playerBackend": playerBackend
|
||||
])
|
||||
case .buffering:
|
||||
sendEvent("onBuffering", ["isBuffering": true])
|
||||
|
|
@ -447,13 +867,86 @@ extension KSPlayerView: KSPlayerLayerDelegate {
|
|||
}
|
||||
|
||||
func player(layer: KSPlayerLayer, currentTime: TimeInterval, totalTime: TimeInterval) {
|
||||
// Debug: Confirm delegate method is being called
|
||||
if currentTime.truncatingRemainder(dividingBy: 10.0) < 0.1 {
|
||||
print("KSPlayerView: [DELEGATE CALLED] time=\(currentTime), total=\(totalTime)")
|
||||
}
|
||||
|
||||
// Manually implement subtitle rendering logic from VideoPlayerView
|
||||
// This is the critical missing piece that was preventing subtitle rendering
|
||||
|
||||
// Debug: Check srtControl state
|
||||
let subtitleInfoCount = playerView.srtControl.subtitleInfos.count
|
||||
let selectedSubtitle = playerView.srtControl.selectedSubtitleInfo
|
||||
|
||||
// Always log subtitle state every 10 seconds to see when it gets populated
|
||||
if currentTime.truncatingRemainder(dividingBy: 10.0) < 0.1 {
|
||||
print("KSPlayerView: [SUBTITLE DEBUG] time=\(currentTime.truncatingRemainder(dividingBy: 10.0)), subtitleInfos=\(subtitleInfoCount), selected=\(selectedSubtitle?.name ?? "none")")
|
||||
|
||||
// Also check if player has subtitle data source
|
||||
let player = layer.player
|
||||
let hasSubtitleDataSource = player.subtitleDataSouce != nil
|
||||
print("KSPlayerView: [SUBTITLE DEBUG] player has subtitle data source: \(hasSubtitleDataSource)")
|
||||
|
||||
// Log subtitle view states
|
||||
print("KSPlayerView: [SUBTITLE DEBUG] subtitleLabel.isHidden: \(playerView.subtitleLabel.isHidden)")
|
||||
print("KSPlayerView: [SUBTITLE DEBUG] subtitleBackView.isHidden: \(playerView.subtitleBackView.isHidden)")
|
||||
print("KSPlayerView: [SUBTITLE DEBUG] subtitleLabel.text: '\(playerView.subtitleLabel.text ?? "nil")'")
|
||||
print("KSPlayerView: [SUBTITLE DEBUG] subtitleLabel.attributedText: \(playerView.subtitleLabel.attributedText != nil ? "exists" : "nil")")
|
||||
print("KSPlayerView: [SUBTITLE DEBUG] subtitleBackView.image: \(playerView.subtitleBackView.image != nil ? "exists" : "nil")")
|
||||
|
||||
// Log all subtitle infos
|
||||
for (index, info) in playerView.srtControl.subtitleInfos.enumerated() {
|
||||
print("KSPlayerView: [SUBTITLE DEBUG] SubtitleInfo[\(index)]: name=\(info.name), isEnabled=\(info.isEnabled)")
|
||||
}
|
||||
}
|
||||
|
||||
let hasSubtitleParts = playerView.srtControl.subtitle(currentTime: currentTime)
|
||||
|
||||
// Debug: Check subtitle timing every 10 seconds
|
||||
if currentTime.truncatingRemainder(dividingBy: 10.0) < 0.1 && subtitleInfoCount > 0 {
|
||||
print("KSPlayerView: [SUBTITLE TIMING] time=\(currentTime), hasParts=\(hasSubtitleParts), partsCount=\(playerView.srtControl.parts.count)")
|
||||
if let firstPart = playerView.srtControl.parts.first {
|
||||
print("KSPlayerView: [SUBTITLE TIMING] firstPart start=\(firstPart.start), end=\(firstPart.end)")
|
||||
print("KSPlayerView: [SUBTITLE TIMING] firstPart text='\(firstPart.text?.string ?? "nil")'")
|
||||
print("KSPlayerView: [SUBTITLE TIMING] firstPart hasImage=\(firstPart.image != nil)")
|
||||
} else {
|
||||
print("KSPlayerView: [SUBTITLE TIMING] No parts available")
|
||||
}
|
||||
}
|
||||
|
||||
if hasSubtitleParts {
|
||||
if let part = playerView.srtControl.parts.first {
|
||||
print("KSPlayerView: [SUBTITLE RENDER] time=\(currentTime), text='\(part.text?.string ?? "nil")', hasImage=\(part.image != nil)")
|
||||
playerView.subtitleBackView.image = part.image
|
||||
playerView.subtitleLabel.attributedText = part.text
|
||||
playerView.subtitleBackView.isHidden = false
|
||||
playerView.subtitleLabel.isHidden = false
|
||||
print("KSPlayerView: [SUBTITLE RENDER] Set subtitle text and made views visible")
|
||||
print("KSPlayerView: [SUBTITLE RENDER] subtitleLabel.isHidden after: \(playerView.subtitleLabel.isHidden)")
|
||||
print("KSPlayerView: [SUBTITLE RENDER] subtitleBackView.isHidden after: \(playerView.subtitleBackView.isHidden)")
|
||||
} else {
|
||||
print("KSPlayerView: [SUBTITLE RENDER] hasParts=true but no parts available - hiding views")
|
||||
playerView.subtitleBackView.image = nil
|
||||
playerView.subtitleLabel.attributedText = nil
|
||||
playerView.subtitleBackView.isHidden = true
|
||||
playerView.subtitleLabel.isHidden = true
|
||||
}
|
||||
} else {
|
||||
// Only log this occasionally to avoid spam
|
||||
if currentTime.truncatingRemainder(dividingBy: 30.0) < 0.1 {
|
||||
print("KSPlayerView: [SUBTITLE RENDER] time=\(currentTime), hasParts=false - no subtitle at this time")
|
||||
}
|
||||
}
|
||||
|
||||
let p = layer.player
|
||||
// Ensure we have valid duration before sending progress updates
|
||||
if totalTime > 0 {
|
||||
sendEvent("onProgress", [
|
||||
"currentTime": currentTime,
|
||||
"duration": totalTime,
|
||||
"bufferTime": p.playableTime
|
||||
"bufferTime": p.playableTime,
|
||||
"airPlayState": getAirPlayState()
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import React
|
|||
@objc(KSPlayerViewManager)
|
||||
class KSPlayerViewManager: RCTViewManager {
|
||||
|
||||
// Not needed for RCTViewManager-based views; events are exported via RCT_EXPORT_VIEW_PROPERTY
|
||||
// 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
|
||||
|
|
@ -96,4 +96,44 @@ class KSPlayerViewManager: RCTViewManager {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -461,7 +461,7 @@
|
|||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app;
|
||||
PRODUCT_NAME = Nuvio;
|
||||
PRODUCT_NAME = "Nuvio";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
|
@ -492,8 +492,8 @@
|
|||
"-lc++",
|
||||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.nuviohub.app;
|
||||
PRODUCT_NAME = Nuvio;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.nuvio.app";
|
||||
PRODUCT_NAME = "Nuvio";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 237 KiB |
|
Before Width: | Height: | Size: 263 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 263 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 263 KiB After Width: | Height: | Size: 1.2 MiB |
|
|
@ -1,97 +1,101 @@
|
|||
<?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.5</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>20</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>
|
||||
</array>
|
||||
<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>
|
||||
<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.6</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>21</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>
|
||||
</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>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>
|
||||
|
|
@ -1,10 +1,5 @@
|
|||
<?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>
|
||||
<dict/>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
<key>EXUpdatesLaunchWaitMs</key>
|
||||
<integer>30000</integer>
|
||||
<key>EXUpdatesRuntimeVersion</key>
|
||||
<string>1.2.5</string>
|
||||
<string>1.2.6</string>
|
||||
<key>EXUpdatesURL</key>
|
||||
<string>https://grim-reyna-tapframe-69970143.koyeb.app/api/manifest</string>
|
||||
</dict>
|
||||
|
|
|
|||
|
|
@ -726,7 +726,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
|
||||
DefaultRenderersFactory renderersFactory =
|
||||
new DefaultRenderersFactory(getContext())
|
||||
.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER)
|
||||
.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF)
|
||||
.setEnableDecoderFallback(true)
|
||||
.forceEnableMediaCodecAsynchronousQueueing();
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,14 @@
|
|||
"https://github.com/tapframe/NuvioStreaming/blob/main/screenshots/search-portrait.png?raw=true"
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"version": "1.2.6",
|
||||
"buildVersion": "21",
|
||||
"date": "2025-10-19",
|
||||
"localizedDescription": "# Version 1.2.6 – Update Notes\n\n### New Features\n- **AirPlay Support (iOS):**\n - Introduced native AirPlay support for compatible playback.\n - Note: **MKV format not supported** due to iOS restrictions.\n- **Last Streamed Link Caching:**\n - The last streamed link is now cached for **1 hour** on the stream screen for faster playback.\n- **KSPlayer Internal Subtitle Support (iOS):**\n - KSPlayer now supports internal subtitles for improved viewing experience.\n\n### PR Merge – Responsive Video Controls by @qarqun\n- **Responsive Sizing:**\n - All controls now scale dynamically based on screen width for better phone and tablet compatibility.\n- **Play/Pause Animation:**\n - Smooth crossfade transitions with scale effects for a polished user experience.\n- **Seek Animations:**\n - Arc sweep animation on seek buttons.\n - Number slide-out showing +10/-10 seconds.\n - Touch feedback with semi-transparent circle flash.\n- **Technical Details:**\n - Button sizes calculated as a percentage of screen width.\n - All animations use `useNativeDriver` for optimal performance.\n - Separate animation references to prevent animation conflicts.\n\n### Fixes\n- Fixed an issue where **Cinemeta Addon** reappeared even after removal from the addon screen.\n\n---\n\n## Changelog & Download\n[View on GitHub](https://github.com/tapframe/NuvioStreaming/releases/tag/1.2.6)\n\nSince the VLClib alone is 45 MB, Android APKs tend to be larger.\n\n🌐 [Official Website](https://tapframe.github.io/NuvioStreaming/)",
|
||||
"downloadURL": "https://github.com/tapframe/NuvioStreaming/releases/download/1.2.6/Stable_1-2-6.ipa",
|
||||
"size": 25700000
|
||||
},
|
||||
{
|
||||
"version": "1.2.5",
|
||||
"buildVersion": "20",
|
||||
|
|
|
|||
209
package-lock.json
generated
|
|
@ -17,6 +17,7 @@
|
|||
"@expo/metro-runtime": "~6.1.2",
|
||||
"@expo/vector-icons": "^15.0.2",
|
||||
"@gorhom/bottom-sheet": "^5.2.6",
|
||||
"@legendapp/list": "^2.0.13",
|
||||
"@lottiefiles/dotlottie-react": "^0.6.5",
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
"@react-native-community/blur": "^4.4.1",
|
||||
|
|
@ -28,8 +29,7 @@
|
|||
"@react-navigation/native-stack": "^7.3.10",
|
||||
"@react-navigation/stack": "^7.2.10",
|
||||
"@sentry/react-native": "~7.3.0",
|
||||
"@shopify/flash-list": "2.0.2",
|
||||
"@supabase/supabase-js": "^2.54.0",
|
||||
"@shopify/flash-list": "^2.1.0",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/react-native-video": "^5.0.20",
|
||||
"axios": "^1.12.2",
|
||||
|
|
@ -67,6 +67,7 @@
|
|||
"posthog-react-native": "^4.4.0",
|
||||
"react": "19.1.0",
|
||||
"react-native": "0.81.4",
|
||||
"react-native-boost": "^0.6.2",
|
||||
"react-native-bottom-tabs": "^0.12.2",
|
||||
"react-native-gesture-handler": "~2.28.0",
|
||||
"react-native-get-random-values": "^1.11.0",
|
||||
|
|
@ -80,17 +81,17 @@
|
|||
"react-native-svg": "15.12.1",
|
||||
"react-native-url-polyfill": "^2.0.0",
|
||||
"react-native-vector-icons": "^10.3.0",
|
||||
"react-native-video": "^6.12.0",
|
||||
"react-native-video": "^6.17.0",
|
||||
"react-native-web": "^0.21.0",
|
||||
"react-native-wheel-color-picker": "^1.3.1",
|
||||
"react-native-worklets": "^0.6.1",
|
||||
"toastify-react-native": "^7.2.3"
|
||||
"react-native-worklets": "^0.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/react": "~18.3.12",
|
||||
"@types/react-native": "^0.72.8",
|
||||
"@types/react-native-vector-icons": "^6.4.18",
|
||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||
"patch-package": "^8.0.1",
|
||||
"react-native-svg-transformer": "^1.5.0",
|
||||
|
|
@ -2347,6 +2348,27 @@
|
|||
"integrity": "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@isaacs/balanced-match": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/brace-expansion": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
|
||||
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@isaacs/balanced-match": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
|
|
@ -2813,6 +2835,19 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@legendapp/list": {
|
||||
"version": "2.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@legendapp/list/-/list-2.0.13.tgz",
|
||||
"integrity": "sha512-OL9rvxRDDqiQ07+QhldcRqCX5+VihtXbbZaoey0TVWJqQN5XPh9b9Buefax3/HjNRzCaYTx1lCoeW5dz20j+cA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"use-sync-external-store": "^1.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@lottiefiles/dotlottie-react": {
|
||||
"version": "0.6.5",
|
||||
"resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-react/-/dotlottie-react-0.6.5.tgz",
|
||||
|
|
@ -3589,13 +3624,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@shopify/flash-list": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@shopify/flash-list/-/flash-list-2.0.2.tgz",
|
||||
"integrity": "sha512-zhlrhA9eiuEzja4wxVvotgXHtqd3qsYbXkQ3rsBfOgbFA9BVeErpDE/yEwtlIviRGEqpuFj/oU5owD6ByaNX+w==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@shopify/flash-list/-/flash-list-2.1.0.tgz",
|
||||
"integrity": "sha512-/EIQlptG456yM5o9qNmNsmaZEFEOGvG3WGyb6GUAxSLlcKUGlPUkPI2NLW5wQSDEY4xSRa5zocUI+9xwmsM4Kg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/runtime": "*",
|
||||
"react": "*",
|
||||
|
|
@ -3626,80 +3658,6 @@
|
|||
"@sinonjs/commons": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/auth-js": {
|
||||
"version": "2.75.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.75.0.tgz",
|
||||
"integrity": "sha512-J8TkeqCOMCV4KwGKVoxmEBuDdHRwoInML2vJilthOo7awVCro2SM+tOcpljORwuBQ1vHUtV62Leit+5wlxrNtw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "2.6.15"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/functions-js": {
|
||||
"version": "2.75.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.75.0.tgz",
|
||||
"integrity": "sha512-18yk07Moj/xtQ28zkqswxDavXC3vbOwt1hDuYM3/7xPnwwpKnsmPyZ7bQ5th4uqiJzQ135t74La9tuaxBR6e7w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "2.6.15"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/node-fetch": {
|
||||
"version": "2.6.15",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz",
|
||||
"integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/postgrest-js": {
|
||||
"version": "2.75.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.75.0.tgz",
|
||||
"integrity": "sha512-YfBz4W/z7eYCFyuvHhfjOTTzRrQIvsMG2bVwJAKEVVUqGdzqfvyidXssLBG0Fqlql1zJFgtsPpK1n4meHrI7tg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "2.6.15"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/realtime-js": {
|
||||
"version": "2.75.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.75.0.tgz",
|
||||
"integrity": "sha512-B4Xxsf2NHd5cEnM6MGswOSPSsZKljkYXpvzKKmNxoUmNQOfB7D8HOa6NwHcUBSlxcjV+vIrYKcYXtavGJqeGrw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "2.6.15",
|
||||
"@types/phoenix": "^1.6.6",
|
||||
"@types/ws": "^8.18.1",
|
||||
"ws": "^8.18.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/storage-js": {
|
||||
"version": "2.75.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.75.0.tgz",
|
||||
"integrity": "sha512-wpJMYdfFDckDiHQaTpK+Ib14N/O2o0AAWWhguKvmmMurB6Unx17GGmYp5rrrqCTf8S1qq4IfIxTXxS4hzrUySg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "2.6.15"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/supabase-js": {
|
||||
"version": "2.75.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.75.0.tgz",
|
||||
"integrity": "sha512-8UN/vATSgS2JFuJlMVr51L3eUDz+j1m7Ww63wlvHLKULzCDaVWYzvacCjBTLW/lX/vedI2LBI4Vg+01G9ufsJQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/auth-js": "2.75.0",
|
||||
"@supabase/functions-js": "2.75.0",
|
||||
"@supabase/node-fetch": "2.6.15",
|
||||
"@supabase/postgrest-js": "2.75.0",
|
||||
"@supabase/realtime-js": "2.75.0",
|
||||
"@supabase/storage-js": "2.75.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@svgr/babel-plugin-add-jsx-attribute": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz",
|
||||
|
|
@ -4224,12 +4182,6 @@
|
|||
"undici-types": "~7.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/phoenix": {
|
||||
"version": "1.6.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
|
||||
"integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||
|
|
@ -4257,6 +4209,27 @@
|
|||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-native-vector-icons": {
|
||||
"version": "6.4.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-native-vector-icons/-/react-native-vector-icons-6.4.18.tgz",
|
||||
"integrity": "sha512-YGlNWb+k5laTBHd7+uZowB9DpIK3SXUneZqAiKQaj1jnJCZM0x71GDim5JCTMi4IFkhc9m8H/Gm28T5BjyivUw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-native": "^0.70"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-native-vector-icons/node_modules/@types/react-native": {
|
||||
"version": "0.70.19",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.70.19.tgz",
|
||||
"integrity": "sha512-c6WbyCgWTBgKKMESj/8b4w+zWcZSsCforson7UdXtXMecG3MxCinYi6ihhrHVPyUrVzORsvEzK8zg32z4pK6Sg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-native-video": {
|
||||
"version": "5.0.20",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-native-video/-/react-native-video-5.0.20.tgz",
|
||||
|
|
@ -4273,15 +4246,6 @@
|
|||
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
"version": "17.0.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
|
||||
|
|
@ -10749,6 +10713,37 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-boost": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-boost/-/react-native-boost-0.6.2.tgz",
|
||||
"integrity": "sha512-6w9PdGvFzyI1dyN516+mLfFF5vETPsjoc26rUFlzWav7PNbC7WV0KyfTBr0q/cDjZkWLMleWQZkGTqSQ1H4PHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.25.0",
|
||||
"@babel/helper-module-imports": "^7.25.0",
|
||||
"@babel/helper-plugin-utils": "^7.25.0",
|
||||
"minimatch": "^10.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-boost/node_modules/minimatch": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
|
||||
"integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@isaacs/brace-expansion": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-bottom-tabs": {
|
||||
"version": "0.12.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-bottom-tabs/-/react-native-bottom-tabs-0.12.2.tgz",
|
||||
|
|
@ -12880,19 +12875,6 @@
|
|||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/toastify-react-native": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/toastify-react-native/-/toastify-react-native-7.2.3.tgz",
|
||||
"integrity": "sha512-ngmpTKlTo0IRddwSsNWK+YKbB2veqotHy7Zpil4eksoLAlq0RPSgdVOk5QDEDUONJQ4r7ljGYeRW68KBztirsg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-native-vector-icons": "*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
|
|
@ -12949,6 +12931,7 @@
|
|||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
|
|
|
|||
11
package.json
|
|
@ -17,6 +17,7 @@
|
|||
"@expo/metro-runtime": "~6.1.2",
|
||||
"@expo/vector-icons": "^15.0.2",
|
||||
"@gorhom/bottom-sheet": "^5.2.6",
|
||||
"@legendapp/list": "^2.0.13",
|
||||
"@lottiefiles/dotlottie-react": "^0.6.5",
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
"@react-native-community/blur": "^4.4.1",
|
||||
|
|
@ -28,8 +29,7 @@
|
|||
"@react-navigation/native-stack": "^7.3.10",
|
||||
"@react-navigation/stack": "^7.2.10",
|
||||
"@sentry/react-native": "~7.3.0",
|
||||
"@shopify/flash-list": "2.0.2",
|
||||
"@supabase/supabase-js": "^2.54.0",
|
||||
"@shopify/flash-list": "^2.1.0",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/react-native-video": "^5.0.20",
|
||||
"axios": "^1.12.2",
|
||||
|
|
@ -67,6 +67,7 @@
|
|||
"posthog-react-native": "^4.4.0",
|
||||
"react": "19.1.0",
|
||||
"react-native": "0.81.4",
|
||||
"react-native-boost": "^0.6.2",
|
||||
"react-native-bottom-tabs": "^0.12.2",
|
||||
"react-native-gesture-handler": "~2.28.0",
|
||||
"react-native-get-random-values": "^1.11.0",
|
||||
|
|
@ -80,17 +81,17 @@
|
|||
"react-native-svg": "15.12.1",
|
||||
"react-native-url-polyfill": "^2.0.0",
|
||||
"react-native-vector-icons": "^10.3.0",
|
||||
"react-native-video": "^6.12.0",
|
||||
"react-native-video": "^6.17.0",
|
||||
"react-native-web": "^0.21.0",
|
||||
"react-native-wheel-color-picker": "^1.3.1",
|
||||
"react-native-worklets": "^0.6.1",
|
||||
"toastify-react-native": "^7.2.3"
|
||||
"react-native-worklets": "^0.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/react": "~18.3.12",
|
||||
"@types/react-native": "^0.72.8",
|
||||
"@types/react-native-vector-icons": "^6.4.18",
|
||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||
"patch-package": "^8.0.1",
|
||||
"react-native-svg-transformer": "^1.5.0",
|
||||
|
|
|
|||
0
patches/react-native-video+6.12.0.patch
Normal file
1467
patches/react-native-video+6.17.0.patch
Normal file
BIN
src/assets/splash-icon-new.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
56
src/components/AnimatedImage.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import React, { memo, useEffect } from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import Animated, {
|
||||
useSharedValue,
|
||||
useAnimatedStyle,
|
||||
withTiming
|
||||
} from 'react-native-reanimated';
|
||||
import FastImage from '@d11/react-native-fast-image';
|
||||
|
||||
interface AnimatedImageProps {
|
||||
source: { uri: string } | undefined;
|
||||
style: any;
|
||||
contentFit: any;
|
||||
onLoad?: () => void;
|
||||
}
|
||||
|
||||
const AnimatedImage = memo(({
|
||||
source,
|
||||
style,
|
||||
contentFit,
|
||||
onLoad
|
||||
}: AnimatedImageProps) => {
|
||||
const opacity = useSharedValue(0);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
opacity: opacity.value,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (source?.uri) {
|
||||
opacity.value = withTiming(1, { duration: 300 });
|
||||
} else {
|
||||
opacity.value = 0;
|
||||
}
|
||||
}, [source?.uri]);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
opacity.value = 0;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Animated.View style={[style, animatedStyle]}>
|
||||
<FastImage
|
||||
source={source}
|
||||
style={StyleSheet.absoluteFillObject}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
onLoad={onLoad}
|
||||
/>
|
||||
</Animated.View>
|
||||
);
|
||||
});
|
||||
|
||||
export default AnimatedImage;
|
||||
50
src/components/AnimatedText.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import React, { memo, useEffect } from 'react';
|
||||
import Animated, {
|
||||
useSharedValue,
|
||||
useAnimatedStyle,
|
||||
withTiming,
|
||||
withDelay
|
||||
} from 'react-native-reanimated';
|
||||
|
||||
interface AnimatedTextProps {
|
||||
children: React.ReactNode;
|
||||
style: any;
|
||||
delay?: number;
|
||||
numberOfLines?: number;
|
||||
}
|
||||
|
||||
const AnimatedText = memo(({
|
||||
children,
|
||||
style,
|
||||
delay = 0,
|
||||
numberOfLines
|
||||
}: AnimatedTextProps) => {
|
||||
const opacity = useSharedValue(0);
|
||||
const translateY = useSharedValue(20);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
opacity: opacity.value,
|
||||
transform: [{ translateY: translateY.value }],
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
opacity.value = withDelay(delay, withTiming(1, { duration: 250 }));
|
||||
translateY.value = withDelay(delay, withTiming(0, { duration: 250 }));
|
||||
}, [delay]);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
opacity.value = 0;
|
||||
translateY.value = 20;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Animated.Text style={[style, animatedStyle]} numberOfLines={numberOfLines}>
|
||||
{children}
|
||||
</Animated.Text>
|
||||
);
|
||||
});
|
||||
|
||||
export default AnimatedText;
|
||||
48
src/components/AnimatedView.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import React, { memo, useEffect } from 'react';
|
||||
import Animated, {
|
||||
useSharedValue,
|
||||
useAnimatedStyle,
|
||||
withTiming,
|
||||
withDelay
|
||||
} from 'react-native-reanimated';
|
||||
|
||||
interface AnimatedViewProps {
|
||||
children: React.ReactNode;
|
||||
style?: any;
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
const AnimatedView = memo(({
|
||||
children,
|
||||
style,
|
||||
delay = 0
|
||||
}: AnimatedViewProps) => {
|
||||
const opacity = useSharedValue(0);
|
||||
const translateY = useSharedValue(20);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
opacity: opacity.value,
|
||||
transform: [{ translateY: translateY.value }],
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
opacity.value = withDelay(delay, withTiming(1, { duration: 250 }));
|
||||
translateY.value = withDelay(delay, withTiming(0, { duration: 250 }));
|
||||
}, [delay]);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
opacity.value = 0;
|
||||
translateY.value = 20;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Animated.View style={[style, animatedStyle]}>
|
||||
{children}
|
||||
</Animated.View>
|
||||
);
|
||||
});
|
||||
|
||||
export default AnimatedView;
|
||||
88
src/components/ProviderFilter.tsx
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import React, { memo, useCallback } from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, FlatList } from 'react-native';
|
||||
|
||||
interface ProviderFilterProps {
|
||||
selectedProvider: string;
|
||||
providers: Array<{ id: string; name: string; }>;
|
||||
onSelect: (id: string) => void;
|
||||
theme: any;
|
||||
}
|
||||
|
||||
const ProviderFilter = memo(({
|
||||
selectedProvider,
|
||||
providers,
|
||||
onSelect,
|
||||
theme
|
||||
}: ProviderFilterProps) => {
|
||||
const styles = React.useMemo(() => createStyles(theme.colors), [theme.colors]);
|
||||
|
||||
const renderItem = useCallback(({ item, index }: { item: { id: string; name: string }; index: number }) => (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.filterChip,
|
||||
selectedProvider === item.id && styles.filterChipSelected
|
||||
]}
|
||||
onPress={() => onSelect(item.id)}
|
||||
>
|
||||
<Text style={[
|
||||
styles.filterChipText,
|
||||
selectedProvider === item.id && styles.filterChipTextSelected
|
||||
]}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
), [selectedProvider, onSelect, styles]);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<FlatList
|
||||
data={providers}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={item => item.id}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
style={styles.filterScroll}
|
||||
bounces={true}
|
||||
overScrollMode="never"
|
||||
decelerationRate="fast"
|
||||
initialNumToRender={5}
|
||||
maxToRenderPerBatch={3}
|
||||
windowSize={3}
|
||||
removeClippedSubviews={true}
|
||||
getItemLayout={(data, index) => ({
|
||||
length: 100, // Approximate width of each item
|
||||
offset: 100 * index,
|
||||
index,
|
||||
})}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
const createStyles = (colors: any) => StyleSheet.create({
|
||||
filterScroll: {
|
||||
flexGrow: 0,
|
||||
},
|
||||
filterChip: {
|
||||
backgroundColor: colors.elevation2,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 16,
|
||||
marginRight: 8,
|
||||
borderWidth: 0,
|
||||
},
|
||||
filterChipSelected: {
|
||||
backgroundColor: colors.primary,
|
||||
},
|
||||
filterChipText: {
|
||||
color: colors.highEmphasis,
|
||||
fontWeight: '600',
|
||||
letterSpacing: 0.1,
|
||||
},
|
||||
filterChipTextSelected: {
|
||||
color: colors.white,
|
||||
fontWeight: '700',
|
||||
},
|
||||
});
|
||||
|
||||
export default ProviderFilter;
|
||||
36
src/components/PulsingChip.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import React, { memo } from 'react';
|
||||
import { View, Text, StyleSheet } from 'react-native';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
|
||||
interface PulsingChipProps {
|
||||
text: string;
|
||||
delay: number;
|
||||
}
|
||||
|
||||
const PulsingChip = memo(({ text, delay }: PulsingChipProps) => {
|
||||
const { currentTheme } = useTheme();
|
||||
const styles = React.useMemo(() => createStyles(currentTheme.colors), [currentTheme.colors]);
|
||||
// Make chip static to avoid continuous animation load
|
||||
return (
|
||||
<View style={styles.activeScraperChip}>
|
||||
<Text style={styles.activeScraperText}>{text}</Text>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
const createStyles = (colors: any) => StyleSheet.create({
|
||||
activeScraperChip: {
|
||||
backgroundColor: colors.elevation2,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 3,
|
||||
borderRadius: 6,
|
||||
borderWidth: 0,
|
||||
},
|
||||
activeScraperText: {
|
||||
color: colors.mediumEmphasis,
|
||||
fontSize: 11,
|
||||
fontWeight: '400',
|
||||
},
|
||||
});
|
||||
|
||||
export default PulsingChip;
|
||||