Compare commits

...

84 commits
1.2.5 ... main

Author SHA1 Message Date
tapframe
4daab74e27 added contributors page 2025-10-24 02:14:50 +05:30
tapframe
a7fbd567fd Added collections ection 2025-10-23 17:31:49 +05:30
tapframe
f90752bdb7 Streamscreen new changes update 2025-10-23 14:51:41 +05:30
tapframe
c5590639b1 revert 2025-10-23 13:44:08 +05:30
tapframe
098ab73ba1 anim changes ios ksplayer 2025-10-23 13:26:43 +05:30
tapframe
060b0b927b episode poster fix 2025-10-23 13:26:31 +05:30
tapframe
786e06b27f episodes not fetching backdrop fix tablet layout 2025-10-23 01:57:04 +05:30
tapframe
ef1c34a9c0 improved animations 2025-10-23 00:38:45 +05:30
tapframe
b97481f2d9 refactor streamscreen 2025-10-23 00:34:08 +05:30
tapframe
8d74b7e7ce changes 2025-10-22 23:55:45 +05:30
tapframe
635c97b1ad more improvements 2025-10-22 23:46:50 +05:30
tapframe
673c96c917 new streamscreen layout for tabs init 2025-10-22 23:36:37 +05:30
tapframe
15fc49d84d cleanup repo 2025-10-22 22:24:27 +05:30
tapframe
54cfd194f1 added yml for sponsoring 2025-10-22 22:22:18 +05:30
tapframe
be561c6d9f adjusted android bottom tab height 2025-10-22 17:25:12 +05:30
tapframe
dc8c27dfc4 trailer service improvements 2025-10-22 17:17:20 +05:30
tapframe
ce7f92b540 Custom TTL for Stream cache 2025-10-21 23:34:35 +05:30
tapframe
f0271cd395 orientation fix 2025-10-21 23:14:51 +05:30
tapframe
2a4c076854 fixed subtitle not fetching on cahced strream link 2025-10-21 22:17:42 +05:30
tapframe
c852c56231 orientation optimization 2025-10-21 17:49:49 +05:30
tapframe
614ffc12c0 1.2.6 21 2025-10-21 16:55:53 +05:30
tapframe
d9b2545cdd streamscreen UI fix 2025-10-21 16:30:39 +05:30
tapframe
1ae6b4f108 continue watching changes 2025-10-21 16:09:35 +05:30
tapframe
373efa0564 Implemented caching stream links for faster playback. 2025-10-21 14:53:01 +05:30
tapframe
6c464abdd4 cleanup 2025-10-21 14:18:24 +05:30
tapframe
5d5d77ae1b sizing fix for playercontrols. 2025-10-21 14:12:20 +05:30
tapframe
b2cfc19e96
Merge pull request #202 from qarqun/main
Responsive Video Player Controls with animations
2025-10-21 13:58:23 +05:30
tapframe
e40e8bb7c5
Merge branch 'main' into main 2025-10-21 13:58:00 +05:30
tapframe
a3158be2bd icon change, and cinemeta removal fix 2025-10-21 13:29:17 +05:30
Qarqun
e305dee777 feat: Add responsive video player controls with animations
- Implement responsive sizing for all controls based on screen width (phone/tablet support)
- Add smooth crossfade animation for play/pause button transitions
- Add arc sweep and slide animations for seek buttons (+10/-10s)
- Add touch feedback with semi-transparent circle flash on tap
2025-10-20 19:13:46 +08:00
tapframe
415efd4e03 asepct ratio fix 2025-10-20 14:45:48 +05:30
tapframe
f027788266 ksplayer subtitle init 2025-10-20 14:08:23 +05:30
qarqun
23acda3167
Merge branch 'tapframe:main' into main 2025-10-20 15:35:59 +08:00
tapframe
a8b4dc5a01 airplay ios 2025-10-20 02:26:42 +05:30
tapframe
fd4efe6c7f catalogscreen layout fix 2025-10-19 20:42:41 +05:30
tapframe
68340eac9e update morelikethis sizing 2025-10-19 20:34:37 +05:30
tapframe
175d47f71f adaptive sizing for playercontrols,seriescontent,and commentsection 2025-10-19 20:30:24 +05:30
tapframe
0b764412b2 homescreen imrpovements for tablet screens 2025-10-19 20:08:08 +05:30
tapframe
f7c0c670d7 metadatascreen tablet layout overhaul 2025-10-19 20:08:08 +05:30
tapframe
18bd6ff3ca macos fullscreen player issue fix 2025-10-19 20:08:08 +05:30
tapframe
ac5326ba3f hero Ui changes 2025-10-19 20:08:08 +05:30
tapframe
c5af56537b
Merge pull request #197 from aazz180/main 2025-10-19 18:03:59 +05:30
aazz180
17cdd503e9
Fix the live container 2025-10-19 13:26:55 +01:00
tapframe
688950d0c2 changes 2025-10-19 15:41:02 +05:30
tapframe
eb3082cddb ui updates trakt 2025-10-19 15:37:30 +05:30
tapframe
32bec08f30 ui changes 2025-10-19 15:19:08 +05:30
tapframe
a7f850d577 trakt wathclist integration test 2025-10-19 14:20:38 +05:30
tapframe
08f356cfa4 trakt scrobble optimization 2025-10-19 13:59:40 +05:30
qarqun
6e975ffe26
Merge branch 'tapframe:main' into main 2025-10-19 15:42:24 +08:00
tapframe
64981dd110 clean 2025-10-19 12:51:07 +05:30
tapframe
b5156bcc69 cleanup 2025-10-19 12:50:14 +05:30
tapframe
9ab99a1225 ios homescreen layout shift fix 2025-10-19 12:49:10 +05:30
tapframe
d5edec025c Merge branch 'main' of https://github.com/tapframe/NuvioStreaming 2025-10-19 12:20:56 +05:30
tapframe
ef43463b99 calender screen fix 2025-10-19 12:19:28 +05:30
qarqun
ca2e95e6f4
Merge branch 'tapframe:main' into main 2025-10-19 12:04:34 +08:00
tapframe
559c50fa87
Merge pull request #193 from aazz180/main 2025-10-19 08:54:45 +05:30
aazz180
2ca0a05636
Add Infuse Livecontainer player option 2025-10-18 21:37:11 +01:00
aazz180
363de47313
Update StreamsScreen.tsx 2025-10-18 21:36:25 +01:00
aazz180
bdb2803371
Update StreamsScreen.tsx 2025-10-18 21:35:59 +01:00
aazz180
6eae438300
Update useSettings.ts 2025-10-18 21:35:28 +01:00
tapframe
707ceb711a layout anim 2025-10-18 23:34:43 +05:30
tapframe
024646579e animate catalogsec 2025-10-18 23:32:57 +05:30
Qarqun
f895428e3d Merge branch 'main' of https://github.com/qarqun/NuvioStreaming 2025-10-19 01:21:48 +08:00
Qarqun
698456c205 refactor(player): enhance video controls with modern streaming style
- Moved inline styles to playerStyles.ts for better maintainability
- Redesigned player controls for better user experience:
  - Enhanced skip buttons with rotate animations and semi-transparent backgrounds
  - Enlarged center play/pause button with improved visibility
  - Optimized touch targets and spacing for better interaction
  - Standardized button dimensions and layout

Files changed:
- src/components/player/controls/PlayerControls.tsx
- src/components/player/utils/playerStyles.ts
2025-10-19 01:11:08 +08:00
tapframe
43cf907a2e icon update 2025-10-18 20:53:42 +05:30
tapframe
0a04ba5743 cache changes 2025-10-18 14:58:22 +05:30
tapframe
8b1a40d2e2 ui changes 2025-10-18 13:22:34 +05:30
tapframe
51ae0784cf flashlist update 2025-10-18 12:09:22 +05:30
tapframe
efa5d3f629 fastimage api bug fix 2025-10-18 00:41:32 +05:30
tapframe
fd5861026d floating header logo fetch fix 2025-10-17 23:41:27 +05:30
tapframe
1535ef9aac trailer improvements 2025-10-17 23:19:55 +05:30
tapframe
bb6f1f32a0 anim changes 2025-10-17 22:18:17 +05:30
tapframe
3effdee5c0 optimzed perf 2025-10-17 22:09:42 +05:30
tapframe
bf15c5fb45 ui change 2025-10-17 22:04:28 +05:30
tapframe
5c3ba9e0d8 parallax on hero 2025-10-17 21:54:02 +05:30
tapframe
ce0b39d48b hero carousal anim changes 2025-10-17 21:44:24 +05:30
tapframe
71e3498876 ui fix 2025-10-17 21:29:15 +05:30
tapframe
e8ec05bd51 rn video init for trailers 2025-10-17 20:35:48 +05:30
tapframe
2303c32940 ui changes 2025-10-17 20:32:46 +05:30
tapframe
e9d54bf0d6 trailer section init 2025-10-17 20:18:34 +05:30
tapframe
e435a68aea UI changes 2025-10-17 15:00:36 +05:30
tapframe
d55143e6fb changes 2025-10-16 12:21:05 +05:30
tapframe
e2719c373d improved vlc behaviour 2025-10-16 01:53:30 +05:30
tapframe
b1e9f9b3f8 ui fix 2025-10-15 23:43:06 +05:30
177 changed files with 13477 additions and 4777 deletions

4
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,4 @@
# These are supported funding model platforms
github: [tapframe]
ko_fi: tapframe

1
.gitignore vendored
View file

@ -72,3 +72,4 @@ SDK54_UPGRADE_SUMMARY.md
SDK54_UPGRADE_SUMMARY.md
build-and-publish-app-releases.sh
bottomnav.md
/TrailerServices

View file

@ -1,2 +1,3 @@
{
"java.compile.nullAnalysis.mode": "automatic"
}

View file

@ -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>

View file

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View file

@ -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>

View file

@ -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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7 KiB

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 85 KiB

View file

@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 718 B

After

Width:  |  Height:  |  Size: 785 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 423 KiB

After

Width:  |  Height:  |  Size: 583 KiB

View file

@ -4,6 +4,7 @@ module.exports = function (api) {
presets: ['babel-preset-expo'],
plugins: [
'react-native-worklets/plugin',
'react-native-boost/plugin',
],
env: {
production: {

View file

@ -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

View file

@ -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")
}
}
}
}

View file

@ -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()
])
}
}

View file

@ -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)")
}
}
}
}

View file

@ -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";

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View file

@ -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>

View file

@ -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>

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

@ -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>

View file

@ -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();

View file

@ -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
View file

@ -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": {

View file

@ -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",

View file

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View 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;

View 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;

View 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;

View 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;

View 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;

Some files were not shown because too many files have changed in this diff Show more