Compare commits
No commits in common. "main" and "1.1.1" have entirely different histories.
11
.env.example
|
|
@ -3,12 +3,6 @@
|
|||
EXPO_PUBLIC_SUPABASE_URL=your_supabase_project_url
|
||||
EXPO_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
|
||||
|
||||
# Remote cache for TMDB (optional)
|
||||
# Set to true to use local/remote cache server, and provide URL
|
||||
EXPO_PUBLIC_USE_REMOTE_CACHE=false
|
||||
EXPO_PUBLIC_CACHE_SERVER_URL=http://localhost:5173
|
||||
EXPO_PUBLIC_DISABLE_LOCAL_CACHE=false
|
||||
|
||||
# MovieBox (MoviesMod) Keys
|
||||
EXPO_PUBLIC_MOVIEBOX_PRIMARY_KEY=your_moviebox_primary_key
|
||||
EXPO_PUBLIC_MOVIEBOX_TMDB_API_KEY=your_tmdb_api_key_for_moviebox
|
||||
|
|
@ -17,8 +11,3 @@ EXPO_PUBLIC_MOVIEBOX_TMDB_API_KEY=your_tmdb_api_key_for_moviebox
|
|||
EXPO_PUBLIC_TRAKT_CLIENT_ID=your_trakt_client_id
|
||||
EXPO_PUBLIC_TRAKT_CLIENT_SECRET=your_trakt_client_secret
|
||||
EXPO_PUBLIC_TRAKT_REDIRECT_URI=stremioexpo://auth/trakt
|
||||
|
||||
# Skip Intro API (IntroDB)
|
||||
# Fetches intro timestamps for TV shows to enable skip intro functionality
|
||||
EXPO_PUBLIC_INTRODB_API_URL=https://api.introdb.app
|
||||
EXPO_PUBLIC_DISCORD_USER_API=
|
||||
4
.github/FUNDING.yml
vendored
|
|
@ -1,4 +0,0 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: [tapframe]
|
||||
ko_fi: tapframe
|
||||
217
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -1,217 +0,0 @@
|
|||
name: Bug report
|
||||
description: Report a reproducible bug (one per issue).
|
||||
title: "[Bug]: "
|
||||
labels:
|
||||
- bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for reporting a bug.
|
||||
|
||||
If we can reproduce it, we can usually fix it. This form is just to get the basics in one place.
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Quick checks
|
||||
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
label: Pre-flight checks
|
||||
options:
|
||||
- label: I searched existing issues and this is not a duplicate.
|
||||
required: true
|
||||
- label: I can reproduce this on the latest release or latest main build.
|
||||
required: false
|
||||
- label: This issue is limited to a single bug (not multiple unrelated problems).
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Version & device
|
||||
|
||||
- type: input
|
||||
id: app_version
|
||||
attributes:
|
||||
label: App version / OTA update ID
|
||||
description: Release version, commit hash, or OTA update ID. You can find your OTA update ID in Settings > App updates > Current version (hold to copy).
|
||||
placeholder: "e.g. 1.2.3, main@abc1234, or an OTA ID"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: install_method
|
||||
attributes:
|
||||
label: Install method
|
||||
options:
|
||||
- GitHub Release APK / IPA
|
||||
- Expo Go
|
||||
- Built from source
|
||||
- Other (please describe below)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: Platform
|
||||
options:
|
||||
- Android phone/tablet
|
||||
- iOS (iPhone/iPad)
|
||||
- Android emulator
|
||||
- iOS Simulator
|
||||
- Other (please describe below)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: device_model
|
||||
attributes:
|
||||
label: Device model
|
||||
description: "Example: iPhone 15 Pro, Pixel 8, Galaxy S23 Ultra, iPad Pro, etc."
|
||||
placeholder: "e.g. iPhone 15 Pro"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: os_version
|
||||
attributes:
|
||||
label: OS version
|
||||
placeholder: "e.g. Android 14, iOS 17.2"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: player_mode
|
||||
attributes:
|
||||
label: Player mode
|
||||
description: If you are using an external player, most playback issues must be reported to that player instead.
|
||||
options:
|
||||
- Internal player (iOS: KSPlayer)
|
||||
- Internal player (Android: ExoPlayer)
|
||||
- Internal player (Android: MPV)
|
||||
- External player
|
||||
- Ask every time
|
||||
- Not sure
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## What happened?
|
||||
|
||||
- type: dropdown
|
||||
id: area
|
||||
attributes:
|
||||
label: Area (tag)
|
||||
description: Pick the closest match. It helps triage.
|
||||
options:
|
||||
- Playback (start/stop/buffering)
|
||||
- Streams / Sources (selection, loading, errors)
|
||||
- Next Episode / Auto-play
|
||||
- Watch Progress (resume, watched state, history)
|
||||
- Subtitles (styling, sync)
|
||||
- Audio tracks
|
||||
- UI / Layout / Animations
|
||||
- Settings
|
||||
- Sync (Trakt / SIMKL / remote)
|
||||
- Downloads
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Exact steps. If it depends on specific content, describe it (movie/series, season/episode, source/addon name) without sharing private links.
|
||||
placeholder: |
|
||||
1. Open ...
|
||||
2. Navigate to ...
|
||||
3. Press ...
|
||||
4. Observe ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
placeholder: "What you expected to happen."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: Actual behavior
|
||||
placeholder: "What actually happened (include any on-screen error text/codes)."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: frequency
|
||||
attributes:
|
||||
label: Frequency
|
||||
options:
|
||||
- Always
|
||||
- Often (more than 50%)
|
||||
- Sometimes
|
||||
- Rarely
|
||||
- Once
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: regression
|
||||
attributes:
|
||||
label: Did this work before?
|
||||
options:
|
||||
- Not sure
|
||||
- Yes, it used to work
|
||||
- No, it never worked
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Extra context (optional)
|
||||
|
||||
- type: textarea
|
||||
id: media_details
|
||||
attributes:
|
||||
label: Media details (optional)
|
||||
description: Only include what you can safely share.
|
||||
placeholder: |
|
||||
- Content type: series/movie
|
||||
- Season/Episode: S1E2
|
||||
- Stream/source: (addon name / source label)
|
||||
- Video format: (if known)
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs (optional but helpful)
|
||||
description: |
|
||||
Not required, but super helpful for playback/crash issues.
|
||||
If you can, include a short snippet from Metro bundler, Xcode, or `adb logcat`.
|
||||
render: shell
|
||||
placeholder: |
|
||||
adb logcat -d | tail -n 300
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: extra
|
||||
attributes:
|
||||
label: Anything else? (optional)
|
||||
description: Screenshots/recordings, related issues, workarounds, etc.
|
||||
validations:
|
||||
required: false
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -1,8 +0,0 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Downloads / Releases
|
||||
url: https://github.com/tapframe/NuvioMobile/releases
|
||||
about: Grab the latest GitHub Release APK/IPA here.
|
||||
- name: Documentation
|
||||
url: https://github.com/tapframe/NuvioMobile/blob/main/README.md
|
||||
about: Read the README for setup and usage details.
|
||||
78
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
|
|
@ -1,78 +0,0 @@
|
|||
name: Feature request
|
||||
description: Suggest an improvement or new feature.
|
||||
title: "[Feature]: "
|
||||
labels:
|
||||
- enhancement
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
One feature request per issue, please. The more real-world your use case is, the easier it is to evaluate.
|
||||
|
||||
Feature requests are reviewed as product proposals first.
|
||||
Please do not open a pull request for a new feature, major UX change, or broad cosmetic update unless a maintainer has explicitly approved it first.
|
||||
Unapproved feature PRs will usually be closed.
|
||||
|
||||
- type: dropdown
|
||||
id: area
|
||||
attributes:
|
||||
label: Area (tag)
|
||||
options:
|
||||
- Playback
|
||||
- Streams / Sources
|
||||
- Next Episode / Auto-play
|
||||
- Watch Progress
|
||||
- Subtitles
|
||||
- Audio
|
||||
- UI / Layout / Animations
|
||||
- Settings
|
||||
- Sync (Trakt / SIMKL / remote)
|
||||
- Downloads
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem statement
|
||||
description: What problem are you trying to solve?
|
||||
placeholder: "I want to be able to..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: proposed
|
||||
attributes:
|
||||
label: Proposed solution
|
||||
description: What would you like the app to do?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: contribution_plan
|
||||
attributes:
|
||||
label: Are you planning to implement this yourself?
|
||||
description: Major features are usually implemented in-house unless approved first.
|
||||
options:
|
||||
- No, this is only a proposal
|
||||
- Maybe, but only if approved first
|
||||
- Yes, but I understand implementation still needs maintainer approval
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives considered (optional)
|
||||
description: Any workarounds or other approaches you considered.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: extra
|
||||
attributes:
|
||||
label: Additional context (optional)
|
||||
description: Mockups, examples from other apps, etc.
|
||||
validations:
|
||||
required: false
|
||||
43
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -1,43 +0,0 @@
|
|||
## Summary
|
||||
|
||||
<!-- What changed in this PR? -->
|
||||
|
||||
## PR type
|
||||
|
||||
<!-- Pick one and delete the others -->
|
||||
- Bug fix
|
||||
- Small maintenance improvement
|
||||
- Docs fix
|
||||
- Approved larger change (link approval below)
|
||||
|
||||
## Why
|
||||
|
||||
<!-- Why this change is needed. Link bug/issue/context. -->
|
||||
|
||||
## Policy check
|
||||
|
||||
<!-- Confirm these before requesting review -->
|
||||
- [ ] This PR is not cosmetic only.
|
||||
- [ ] This PR does not add a new major feature without prior approval.
|
||||
- [ ] This PR is small in scope and focused on one problem.
|
||||
- [ ] If this is a larger or directional change, I linked the issue where it was approved.
|
||||
|
||||
<!-- PRs that do not match this policy will usually be closed without merge. -->
|
||||
|
||||
## Testing
|
||||
|
||||
<!-- What you tested and how (manual + automated). -->
|
||||
- [ ] iOS tested
|
||||
- [ ] Android tested
|
||||
|
||||
## Screenshots / Video (UI changes only)
|
||||
|
||||
<!-- If UI changed, add before/after screenshots or a short clip. -->
|
||||
|
||||
## Breaking changes
|
||||
|
||||
<!-- Any breaking behavior/config/schema changes? If none, write: None -->
|
||||
|
||||
## Linked issues
|
||||
|
||||
<!-- Example: Fixes #123 -->
|
||||
65
.gitignore
vendored
|
|
@ -2,10 +2,6 @@
|
|||
|
||||
# dependencies
|
||||
node_modules/
|
||||
# Un-ignore specific react-native-video source files we patch
|
||||
!node_modules/react-native-video/android/src/main/java/com/brentvatne/common/api/SubtitleStyle.kt
|
||||
!node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.kt
|
||||
!node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java
|
||||
|
||||
# Expo
|
||||
.expo/
|
||||
|
|
@ -34,12 +30,8 @@ yarn-error.*
|
|||
*.pem
|
||||
|
||||
# local env files
|
||||
|
||||
.env*.local
|
||||
.env
|
||||
# Sentry
|
||||
ios/sentry.properties
|
||||
android/sentry.properties
|
||||
.env*.local
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
|
@ -48,15 +40,11 @@ release_announcement.md
|
|||
ALPHA_BUILD_2_ANNOUNCEMENT.md
|
||||
CHANGELOG.md
|
||||
.env.local
|
||||
# Android build artifacts (but keep source files)
|
||||
android/app/build/
|
||||
android/build/
|
||||
android/.gradle/
|
||||
android/app/libs/*.aar
|
||||
!android/app/libs/lib-decoder-ffmpeg-release.aar
|
||||
!android/app/libs/libmpv-release.aar
|
||||
android/
|
||||
HEATING_OPTIMIZATIONS.md
|
||||
# sliderreadme.md
|
||||
|
||||
android
|
||||
sliderreadme.md
|
||||
.cursor/mcp.json
|
||||
local-scrapers-repo
|
||||
worki.json
|
||||
|
|
@ -65,45 +53,4 @@ hackintosh-emulator-fix.sh
|
|||
/ota-builds
|
||||
src/screens/xavio.md
|
||||
/nuvio-providers
|
||||
/KSPlayer
|
||||
/exobase
|
||||
# ffmpegreadme.md
|
||||
toast.md
|
||||
ffmpegreadme.md
|
||||
sliderreadme.md
|
||||
bottomsheet.md
|
||||
fastimage.md
|
||||
|
||||
## Backup directories
|
||||
backup_sdk54_upgrade/
|
||||
SDK54_UPGRADE_SUMMARY.md
|
||||
SDK54_UPGRADE_SUMMARY.md
|
||||
build-and-publish-app-releases.sh
|
||||
bottomnav.md
|
||||
/TrailerServices
|
||||
mmkv.md
|
||||
fix-android-scroll-lag-summary.md
|
||||
server/cache-server
|
||||
server/campaign-manager
|
||||
server/sync-service
|
||||
carousal.md
|
||||
node_modules
|
||||
expofs.md
|
||||
ios/sentry.properties
|
||||
android/sentry.properties
|
||||
Stremio addons refer
|
||||
trakt-docs
|
||||
trakt-docss
|
||||
|
||||
# Removed submodules (kept locally)
|
||||
libmpv-android/
|
||||
mpv-android/
|
||||
mpvKt/
|
||||
|
||||
# Torrent libraries
|
||||
LibTorrent/
|
||||
iTorrent/
|
||||
simkl-docss
|
||||
downloader.md
|
||||
server
|
||||
Deliverables 2
|
||||
/KSPlayer
|
||||
1
.vscode/settings.json
vendored
|
|
@ -1,3 +1,2 @@
|
|||
{
|
||||
"java.compile.nullAnalysis.mode": "automatic"
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
# Nuvio Alpha Build 2
|
||||
|
||||
This is the second alpha release of Nuvio!
|
||||
|
||||
## What's New
|
||||
- **Intro Submission:** You can now submit intro timestamps directly to IntroDB!
|
||||
- **Bug Fixes:** Various improvements and stability fixes.
|
||||
|
||||
## Installation
|
||||
Download the attached APK and install it on your Android device.
|
||||
262
App.tsx
|
|
@ -10,83 +10,44 @@ import {
|
|||
View,
|
||||
StyleSheet,
|
||||
I18nManager,
|
||||
Platform,
|
||||
LogBox,
|
||||
Linking
|
||||
Platform
|
||||
} from 'react-native';
|
||||
import './src/i18n'; // Initialize i18n
|
||||
import { NavigationContainer } from '@react-navigation/native';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { Provider as PaperProvider } from 'react-native-paper';
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
import { enableScreens, enableFreeze } from 'react-native-screens';
|
||||
import AppNavigator, {
|
||||
import { enableScreens } from 'react-native-screens';
|
||||
import AppNavigator, {
|
||||
CustomNavigationDarkTheme,
|
||||
CustomDarkTheme
|
||||
} from './src/navigation/AppNavigator';
|
||||
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
|
||||
import 'react-native-reanimated';
|
||||
import { CatalogProvider } from './src/contexts/CatalogContext';
|
||||
import { GenreProvider } from './src/contexts/GenreContext';
|
||||
import { TraktProvider } from './src/contexts/TraktContext';
|
||||
import { SimklProvider } from './src/contexts/SimklContext';
|
||||
import { ThemeProvider, useTheme } from './src/contexts/ThemeContext';
|
||||
import { TrailerProvider } from './src/contexts/TrailerContext';
|
||||
import { DownloadsProvider } from './src/contexts/DownloadsContext';
|
||||
import SplashScreen from './src/components/SplashScreen';
|
||||
import UpdatePopup from './src/components/UpdatePopup';
|
||||
import MajorUpdateOverlay from './src/components/MajorUpdateOverlay';
|
||||
import { useGithubMajorUpdate } from './src/hooks/useGithubMajorUpdate';
|
||||
import { useUpdatePopup } from './src/hooks/useUpdatePopup';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import * as Sentry from '@sentry/react-native';
|
||||
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';
|
||||
import { mmkvStorage } from './src/services/mmkvStorage';
|
||||
import { CampaignManager } from './src/components/promotions/CampaignManager';
|
||||
import { isErrorReportingEnabledSync } from './src/services/telemetryService';
|
||||
import { supabaseSyncService } from './src/services/supabaseSyncService';
|
||||
|
||||
// Initialize Sentry with privacy-first defaults
|
||||
// Settings are loaded from telemetryService and can be controlled by user
|
||||
// Note: Full dynamic control requires app restart as Sentry initializes at startup
|
||||
Sentry.init({
|
||||
dsn: 'https://1a58bf436454d346e5852b7bfd3c95e8@o4509536317276160.ingest.de.sentry.io/4509536317734992',
|
||||
|
||||
// Privacy-first: Disable PII by default (IP address, cookies, user data)
|
||||
// Users can opt-in via Privacy Settings if they choose
|
||||
sendDefaultPii: false,
|
||||
// Adds more context data to events (IP address, cookies, user, etc.)
|
||||
// For more information, visit: https://docs.sentry.io/platforms/react-native/data-management/data-collected/
|
||||
sendDefaultPii: true,
|
||||
|
||||
// Session Replay completely disabled by default for privacy
|
||||
// This prevents screen recording without explicit user consent
|
||||
replaysSessionSampleRate: 0,
|
||||
replaysOnErrorSampleRate: 0,
|
||||
|
||||
// Only include feedback integration (user-initiated, not automatic)
|
||||
// Configure Session Replay conservatively to avoid startup overhead in production
|
||||
replaysSessionSampleRate: __DEV__ ? 0.1 : 0,
|
||||
replaysOnErrorSampleRate: __DEV__ ? 1 : 0,
|
||||
integrations: [Sentry.feedbackIntegration()],
|
||||
|
||||
// beforeSend hook to respect user's telemetry preferences
|
||||
// Uses synchronous MMKV read to check preference immediately
|
||||
beforeSend: (event) => {
|
||||
// Check if error reporting is disabled (synchronous check)
|
||||
if (!isErrorReportingEnabledSync()) {
|
||||
// Drop the event - user has opted out
|
||||
return null;
|
||||
}
|
||||
return event;
|
||||
},
|
||||
|
||||
// beforeSendTransaction hook for performance monitoring
|
||||
beforeSendTransaction: (event) => {
|
||||
if (!isErrorReportingEnabledSync()) {
|
||||
return null;
|
||||
}
|
||||
return event;
|
||||
},
|
||||
|
||||
// uncomment the line below to enable Spotlight (https://spotlightjs.com)
|
||||
// spotlight: __DEV__,
|
||||
});
|
||||
|
|
@ -96,16 +57,8 @@ Sentry.init({
|
|||
I18nManager.allowRTL(false);
|
||||
I18nManager.forceRTL(false);
|
||||
|
||||
// Suppress duplicate key warnings app-wide
|
||||
LogBox.ignoreLogs([
|
||||
'Warning: Encountered two children with the same key',
|
||||
'Keys should be unique so that components maintain their identity across updates'
|
||||
]);
|
||||
|
||||
// This fixes many navigation layout issues by using native screen containers
|
||||
enableScreens(true);
|
||||
// Freeze non-focused screens to stop background re-renders
|
||||
enableFreeze(true);
|
||||
|
||||
// Inner app component that uses the theme context
|
||||
const ThemedApp = () => {
|
||||
|
|
@ -115,12 +68,12 @@ const ThemedApp = () => {
|
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const engine = (global as any).HermesInternal ? 'Hermes' : 'JSC';
|
||||
console.log('JS Engine:', engine);
|
||||
} catch { }
|
||||
} catch {}
|
||||
}, []);
|
||||
const { currentTheme } = useTheme();
|
||||
const [isAppReady, setIsAppReady] = useState(false);
|
||||
const [hasCompletedOnboarding, setHasCompletedOnboarding] = useState<boolean | null>(null);
|
||||
|
||||
|
||||
// Update popup functionality
|
||||
const {
|
||||
showUpdatePopup,
|
||||
|
|
@ -130,87 +83,38 @@ const ThemedApp = () => {
|
|||
handleUpdateLater,
|
||||
handleDismiss,
|
||||
} = useUpdatePopup();
|
||||
|
||||
// GitHub major/minor release overlay
|
||||
const githubUpdate = useGithubMajorUpdate();
|
||||
const [isDownloadingGitHub, setIsDownloadingGitHub] = useState(false);
|
||||
const [downloadProgress, setDownloadProgress] = useState(0);
|
||||
|
||||
const handleGithubUpdateAction = async () => {
|
||||
console.log('handleGithubUpdateAction triggered. Release data exists:', !!githubUpdate.releaseData);
|
||||
if (Platform.OS === 'android') {
|
||||
setIsDownloadingGitHub(true);
|
||||
setDownloadProgress(0);
|
||||
try {
|
||||
const { default: AndroidUpdateService } = await import('./src/services/androidUpdateService');
|
||||
if (githubUpdate.releaseData) {
|
||||
console.log('Calling AndroidUpdateService with:', githubUpdate.releaseData.tag_name);
|
||||
const success = await AndroidUpdateService.downloadAndInstallUpdate(
|
||||
githubUpdate.releaseData,
|
||||
(progress) => {
|
||||
setDownloadProgress(progress);
|
||||
}
|
||||
);
|
||||
console.log('AndroidUpdateService result:', success);
|
||||
if (!success) {
|
||||
console.log('Update failed, falling back to browser');
|
||||
// If download fails or no APK found, fallback to browser
|
||||
if (githubUpdate.releaseUrl) Linking.openURL(githubUpdate.releaseUrl);
|
||||
}
|
||||
} else if (githubUpdate.releaseUrl) {
|
||||
console.log('No release data, falling back to browser');
|
||||
Linking.openURL(githubUpdate.releaseUrl);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update via Android service', error);
|
||||
if (githubUpdate.releaseUrl) Linking.openURL(githubUpdate.releaseUrl);
|
||||
} finally {
|
||||
setIsDownloadingGitHub(false);
|
||||
setDownloadProgress(0);
|
||||
}
|
||||
} else {
|
||||
if (githubUpdate.releaseUrl) Linking.openURL(githubUpdate.releaseUrl);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Check onboarding status and initialize services
|
||||
useEffect(() => {
|
||||
const initializeApp = async () => {
|
||||
try {
|
||||
// Check onboarding status
|
||||
const onboardingCompleted = await mmkvStorage.getItem('hasCompletedOnboarding');
|
||||
const onboardingCompleted = await AsyncStorage.getItem('hasCompletedOnboarding');
|
||||
setHasCompletedOnboarding(onboardingCompleted === 'true');
|
||||
|
||||
// Initialize Supabase auth/session and start background sync.
|
||||
// This is intentionally non-blocking for app startup UX.
|
||||
supabaseSyncService
|
||||
.initialize()
|
||||
.then(() => supabaseSyncService.startupSync())
|
||||
.catch((error) => {
|
||||
console.warn('[App] Supabase sync bootstrap failed:', error);
|
||||
});
|
||||
|
||||
// Initialize update service
|
||||
await UpdateService.initialize();
|
||||
|
||||
|
||||
// Initialize update service (skip on Android to prevent update checks)
|
||||
if (Platform.OS !== 'android') {
|
||||
await UpdateService.initialize();
|
||||
}
|
||||
|
||||
// Initialize memory monitoring service to prevent OutOfMemoryError
|
||||
memoryMonitorService; // Just accessing it starts the monitoring
|
||||
console.log('Memory monitoring service initialized');
|
||||
|
||||
|
||||
// Initialize AI service
|
||||
await aiService.initialize();
|
||||
console.log('AI service initialized');
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error initializing app:', error);
|
||||
// Default to showing onboarding if we can't check
|
||||
setHasCompletedOnboarding(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
initializeApp();
|
||||
}, []);
|
||||
|
||||
|
||||
// Create custom themes based on current theme
|
||||
const customDarkTheme = {
|
||||
...CustomDarkTheme,
|
||||
|
|
@ -219,7 +123,7 @@ const ThemedApp = () => {
|
|||
primary: currentTheme.colors.primary,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const customNavigationTheme = {
|
||||
...CustomNavigationDarkTheme,
|
||||
colors: {
|
||||
|
|
@ -234,87 +138,57 @@ const ThemedApp = () => {
|
|||
const handleSplashComplete = () => {
|
||||
setIsAppReady(true);
|
||||
};
|
||||
|
||||
// Navigation reference
|
||||
const navigationRef = React.useRef<any>(null);
|
||||
|
||||
|
||||
// Don't render anything until we know the onboarding status
|
||||
const shouldShowApp = isAppReady && hasCompletedOnboarding !== null;
|
||||
const initialRouteName = hasCompletedOnboarding ? 'MainTabs' : 'Onboarding';
|
||||
|
||||
|
||||
return (
|
||||
<AccountProvider>
|
||||
<PaperProvider theme={customDarkTheme}>
|
||||
<NavigationContainer
|
||||
ref={navigationRef}
|
||||
theme={customNavigationTheme}
|
||||
linking={{
|
||||
prefixes: ['nuvio://'],
|
||||
config: {
|
||||
screens: {
|
||||
ScraperSettings: {
|
||||
path: 'repo',
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DownloadsProvider>
|
||||
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
<StatusBar style="light" />
|
||||
{!isAppReady && <SplashScreen onFinish={handleSplashComplete} />}
|
||||
{shouldShowApp && <AppNavigator initialRouteName={initialRouteName} />}
|
||||
<UpdatePopup
|
||||
visible={showUpdatePopup}
|
||||
updateInfo={updateInfo}
|
||||
onUpdateNow={handleUpdateNow}
|
||||
onUpdateLater={handleUpdateLater}
|
||||
onDismiss={handleDismiss}
|
||||
isInstalling={isInstalling}
|
||||
/>
|
||||
<MajorUpdateOverlay
|
||||
visible={githubUpdate.visible}
|
||||
latestTag={githubUpdate.latestTag}
|
||||
releaseNotes={githubUpdate.releaseNotes}
|
||||
releaseUrl={githubUpdate.releaseUrl}
|
||||
onDismiss={githubUpdate.onDismiss}
|
||||
onLater={githubUpdate.onLater}
|
||||
onUpdateAction={handleGithubUpdateAction}
|
||||
isDownloading={isDownloadingGitHub}
|
||||
downloadProgress={downloadProgress}
|
||||
/>
|
||||
<CampaignManager />
|
||||
</View>
|
||||
</DownloadsProvider>
|
||||
</NavigationContainer>
|
||||
</PaperProvider>
|
||||
</AccountProvider>
|
||||
<PaperProvider theme={customDarkTheme}>
|
||||
<NavigationContainer
|
||||
theme={customNavigationTheme}
|
||||
// Disable automatic linking which can cause layout issues
|
||||
linking={undefined}
|
||||
>
|
||||
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
<StatusBar
|
||||
style="light"
|
||||
/>
|
||||
{!isAppReady && <SplashScreen onFinish={handleSplashComplete} />}
|
||||
{shouldShowApp && <AppNavigator initialRouteName={initialRouteName} />}
|
||||
|
||||
{/* Update Popup */}
|
||||
{Platform.OS === 'ios' && (
|
||||
<UpdatePopup
|
||||
visible={showUpdatePopup}
|
||||
updateInfo={updateInfo}
|
||||
onUpdateNow={handleUpdateNow}
|
||||
onUpdateLater={handleUpdateLater}
|
||||
onDismiss={handleDismiss}
|
||||
isInstalling={isInstalling}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</NavigationContainer>
|
||||
</PaperProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function App(): React.JSX.Element {
|
||||
return (
|
||||
<SafeAreaProvider>
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<BottomSheetModalProvider>
|
||||
<GenreProvider>
|
||||
<CatalogProvider>
|
||||
<TraktProvider>
|
||||
<SimklProvider>
|
||||
<ThemeProvider>
|
||||
<TrailerProvider>
|
||||
<ToastProvider>
|
||||
<ThemedApp />
|
||||
</ToastProvider>
|
||||
</TrailerProvider>
|
||||
</ThemeProvider>
|
||||
</SimklProvider>
|
||||
</TraktProvider>
|
||||
</CatalogProvider>
|
||||
</GenreProvider>
|
||||
</BottomSheetModalProvider>
|
||||
</GestureHandlerRootView>
|
||||
</SafeAreaProvider>
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<GenreProvider>
|
||||
<CatalogProvider>
|
||||
<TraktProvider>
|
||||
<ThemeProvider>
|
||||
<TrailerProvider>
|
||||
<ThemedApp />
|
||||
</TrailerProvider>
|
||||
</ThemeProvider>
|
||||
</TraktProvider>
|
||||
</CatalogProvider>
|
||||
</GenreProvider>
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -324,4 +198,4 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
export default Sentry.wrap(App);
|
||||
export default Sentry.wrap(App);
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
# Contributing
|
||||
|
||||
Thanks for helping improve NuvioMobile.
|
||||
|
||||
## PR policy
|
||||
|
||||
Pull requests are currently intended for:
|
||||
|
||||
- Reproducible bug fixes
|
||||
- Small stability improvements
|
||||
- Minor maintenance work
|
||||
- Small documentation fixes that improve accuracy
|
||||
|
||||
Pull requests are generally **not** accepted for:
|
||||
|
||||
- New major features
|
||||
- Product direction changes
|
||||
- Large UX / UI redesigns
|
||||
- Cosmetic-only changes
|
||||
- Refactors without a clear user-facing or maintenance benefit
|
||||
|
||||
For feature ideas and bigger changes, please open an issue first. Feature implementation is usually kept in-house unless it has been discussed and explicitly approved beforehand.
|
||||
|
||||
## Where to ask questions
|
||||
|
||||
- Use **Issues** for bugs, feature requests, setup help, and general support.
|
||||
|
||||
## Bug reports (rules)
|
||||
|
||||
To keep issues fixable, bug reports should include:
|
||||
|
||||
- App version or OTA update ID (Settings > App updates > Current version, hold to copy)
|
||||
- Platform + device model + OS version (Android/iOS)
|
||||
- Install method (release APK/IPA / Expo Go / built from source)
|
||||
- Steps to reproduce (exact steps)
|
||||
- Expected vs actual behavior
|
||||
- Frequency (always/sometimes/once)
|
||||
|
||||
Logs are **optional**, but they help a lot for playback/crash issues.
|
||||
|
||||
### How to capture logs (optional)
|
||||
|
||||
If you can, reproduce the issue once, then attach a short log snippet from around the time it happened:
|
||||
|
||||
For Android:
|
||||
```sh
|
||||
adb logcat -d | tail -n 300
|
||||
```
|
||||
For iOS/Metro:
|
||||
```sh
|
||||
# Copy from your Metro bundler output or Xcode console
|
||||
```
|
||||
|
||||
If the issue is a crash, also include any stack trace shown by Android Studio, Xcode, or `adb logcat`.
|
||||
|
||||
## Feature requests (rules)
|
||||
|
||||
Please include:
|
||||
|
||||
- The problem you are solving (use case)
|
||||
- Your proposed solution
|
||||
- Alternatives considered (if any)
|
||||
|
||||
Opening a feature request does **not** mean a pull request will be accepted for it. If the feature affects product scope, UX direction, or adds a significant new surface area, do not start implementation unless a maintainer explicitly approves it first.
|
||||
|
||||
## Before opening a PR
|
||||
|
||||
Please make sure your PR is all of the following:
|
||||
|
||||
- Small in scope
|
||||
- Focused on one problem
|
||||
- Clearly aligned with the current direction of the project
|
||||
- Not cosmetic-only
|
||||
- Not a new major feature unless it was discussed and approved first
|
||||
|
||||
PRs that do not fit this policy will usually be closed without merge so review time can stay focused on bugs, regressions, and small improvements.
|
||||
|
||||
## One issue per problem
|
||||
|
||||
Please open separate issues for separate bugs/features. It makes tracking, fixing, and closing issues much faster.
|
||||
180
README.md
|
|
@ -1,85 +1,153 @@
|
|||
<div align="center">
|
||||
# Nuvio Streaming App
|
||||
|
||||
<img src="https://github.com/tapframe/NuvioTV/blob/main/assets/brand/app_logo_wordmark.png" alt="Nuvio" width="300" />
|
||||
<br />
|
||||
<br />
|
||||
<p align="center">
|
||||
<img src="assets/titlelogo.png" alt="Nuvio Logo" width="300"/>
|
||||
</p>
|
||||
|
||||
[![Contributors][contributors-shield]][contributors-url]
|
||||
[![Forks][forks-shield]][forks-url]
|
||||
[![Stargazers][stars-shield]][stars-url]
|
||||
[![Issues][issues-shield]][issues-url]
|
||||
[![License][license-shield]][license-url]
|
||||
<p align="center">
|
||||
A modern streaming application built with React Native and Expo, featuring comprehensive Stremio addon integration and Trakt.tv synchronization.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
A modern media hub for Android and iOS built with React Native and Expo.
|
||||
<br />
|
||||
Stremio Addon ecosystem • Cross-platform
|
||||
</p>
|
||||
---
|
||||
|
||||
</div>
|
||||
## Stable Release
|
||||
Nuvio is now available as a stable release! Version 1.0.0 brings all major features from the beta phase, refined and optimized for the best user experience.
|
||||
|
||||
## About
|
||||
[Download Latest Release](https://github.com/tapframe/NuvioStreaming/tags)
|
||||
|
||||
Nuvio Media Hub is a cross-platform app for managing and discovering media, with a playback-focused interface that can integrate with the Stremio addon ecosystem through user-installed extensions.
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
### Android
|
||||
### AltStore Installation
|
||||
<img src="https://upload.wikimedia.org/wikipedia/commons/2/20/AltStore_logo.png" width="32" height="32" align="left"> [](https://tinyurl.com/NuvioAltstore)
|
||||
|
||||
Download the latest APK from [GitHub Releases](https://github.com/tapframe/NuvioStreaming/releases/latest).
|
||||
### SideStore Installation
|
||||
<img src="https://github.com/SideStore/assets/blob/main/icon.png?raw=true" width="32" height="32" align="left"> [](https://tinyurl.com/NuvioSidestore)
|
||||
|
||||
### iOS
|
||||
**Manual URL:** `https://raw.githubusercontent.com/tapframe/NuvioStreaming/main/nuvio-source.json`
|
||||
|
||||
- [TestFlight](https://testflight.apple.com/join/QkKMGRqp)
|
||||
- [AltStore](https://tinyurl.com/NuvioAltstore)
|
||||
- [SideStore](https://tinyurl.com/NuvioSidestore)
|
||||
---
|
||||
|
||||
**Manual source:** `https://raw.githubusercontent.com/tapframe/NuvioStreaming/main/nuvio-source.json`
|
||||
## Core Features
|
||||
|
||||
### Content Discovery
|
||||
- **Personalized Recommendations:** Smart home screen with continue watching functionality
|
||||
- **Advanced Search:** Comprehensive filtering and instant search results
|
||||
- **Rich Metadata:** Detailed content information, cast, crew, and ratings
|
||||
- **Calendar Integration:** Track upcoming episodes and releases
|
||||
- **Library Management:** Personal collections with watch history tracking
|
||||
|
||||
### Streaming Experience
|
||||
- **Dual Player Support:** Built-in video player with gesture controls and external player integration
|
||||
- **Intelligent Quality Selection:** Automatic stream optimization and subtitle support
|
||||
- **Trailer Playback:** Seamless trailer integration with preloading
|
||||
- **Continuous Playback:** Auto-play functionality for uninterrupted viewing
|
||||
- **Progress Synchronization:** Resume playback across devices
|
||||
|
||||
### Platform Integrations
|
||||
- **Trakt.tv Sync:** Complete watch history, ratings, and library synchronization
|
||||
- **Stremio Ecosystem:** Full addon compatibility with easy management
|
||||
- **TMDB Integration:** Comprehensive metadata and high-quality imagery
|
||||
- **MDBList Support:** Enhanced ratings and recommendations
|
||||
- **Custom Scrapers:** Local content source integration
|
||||
|
||||
### User Interface
|
||||
- **Material Design:** Clean, modern interface with smooth animations
|
||||
- **Dynamic Theming:** Content-based color extraction and customization
|
||||
- **Cross-Platform:** Native experience on iOS and Android
|
||||
- **Performance Optimized:** Efficient rendering and image caching
|
||||
|
||||
### Advanced Features
|
||||
- **Push Notifications:** New episode alerts and content reminders
|
||||
- **Background Synchronization:** Automatic data updates and content sync
|
||||
|
||||
---
|
||||
|
||||
## Screenshots
|
||||
|
||||
| Home Screen | Details Page | Home Screen 2 |
|
||||
|:-----------:|:------------:|:-------------:|
|
||||
|  |  |  |
|
||||
|
||||
| Library | Player Loading | Video Player |
|
||||
|:-------:|:--------------:|:------------:|
|
||||
|  |  |  |
|
||||
|
||||
| Ratings | Episodes & Seasons | Search & Details |
|
||||
|:-------:|:------------------:|:----------------:|
|
||||
|  |  |  |
|
||||
|
||||
---
|
||||
|
||||
## Tools & Technologies
|
||||
|
||||
<p align="left">
|
||||
<a href="https://skillicons.dev">
|
||||
<img src="https://skillicons.dev/icons?i=react,typescript,nodejs,expo,github,githubactions&theme=light&perline=6" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
- Node.js 18+
|
||||
- npm or yarn
|
||||
- Expo CLI
|
||||
|
||||
### Setup
|
||||
```bash
|
||||
git clone https://github.com/tapframe/NuvioStreaming.git
|
||||
cd NuvioStreaming
|
||||
npm install --legacy-peer-deps
|
||||
npx expo prebuild
|
||||
npx expo run:android
|
||||
# or
|
||||
npx expo run:ios
|
||||
npm install
|
||||
npx expo start
|
||||
```
|
||||
|
||||
## Legal & DMCA
|
||||
### Build Commands
|
||||
```bash
|
||||
npx expo run:android # Android build
|
||||
npx expo run:ios # iOS build
|
||||
```
|
||||
|
||||
Nuvio functions solely as a client-side interface for browsing metadata and playing media provided by user-installed extensions and/or user-provided sources. It is intended for content the user owns or is otherwise authorized to access.
|
||||
---
|
||||
|
||||
Nuvio is not affiliated with any third-party extensions, catalogs, sources, or content providers. It does not host, store, or distribute any media content.
|
||||
## Contributing
|
||||
|
||||
For comprehensive legal information, including our full disclaimer, third-party extension policy, and DMCA/Copyright information, please visit our **[Legal & Disclaimer Page](https://nuvioapp.space/legal)**.
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Submit a pull request
|
||||
|
||||
## Built With
|
||||
---
|
||||
|
||||
- React Native
|
||||
- Expo
|
||||
- TypeScript
|
||||
## Issues
|
||||
|
||||
## Star History
|
||||
Report bugs and request features via [GitHub Issues](https://github.com/tapframe/NuvioStreaming/issues)
|
||||
|
||||
<a href="https://www.star-history.com/#tapframe/NuvioStreaming&type=date&legend=top-left">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=tapframe/NuvioStreaming&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=tapframe/NuvioStreaming&type=date&legend=top-left" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=tapframe/NuvioStreaming&type=date&legend=top-left" />
|
||||
</picture>
|
||||
</a>
|
||||
---
|
||||
|
||||
<!-- MARKDOWN LINKS & IMAGES -->
|
||||
[contributors-shield]: https://img.shields.io/github/contributors/tapframe/NuvioStreaming.svg?style=for-the-badge
|
||||
[contributors-url]: https://github.com/tapframe/NuvioStreaming/graphs/contributors
|
||||
[forks-shield]: https://img.shields.io/github/forks/tapframe/NuvioStreaming.svg?style=for-the-badge
|
||||
[forks-url]: https://github.com/tapframe/NuvioStreaming/network/members
|
||||
[stars-shield]: https://img.shields.io/github/stars/tapframe/NuvioStreaming.svg?style=for-the-badge
|
||||
[stars-url]: https://github.com/tapframe/NuvioStreaming/stargazers
|
||||
[issues-shield]: https://img.shields.io/github/issues/tapframe/NuvioStreaming.svg?style=for-the-badge
|
||||
[issues-url]: https://github.com/tapframe/NuvioStreaming/issues
|
||||
[license-shield]: https://img.shields.io/github/license/tapframe/NuvioStreaming.svg?style=for-the-badge
|
||||
[license-url]: http://www.gnu.org/licenses/gpl-3.0.en.html
|
||||
|
||||
|
||||
## License
|
||||
|
||||
[](http://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
---
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This application functions as a content aggregator, accessing publicly available streams from third-party sources. No media content is hosted by this application. Users are responsible for compliance with applicable laws and regulations.
|
||||
|
||||
---
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
Built with support from:
|
||||
- React Native & Expo
|
||||
- TMDB API
|
||||
- Trakt.tv
|
||||
- Stremio
|
||||
|
|
@ -14,7 +14,6 @@ react {
|
|||
hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc"
|
||||
codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
|
||||
|
||||
enableBundleCompression = (findProperty('android.enableBundleCompression') ?: false).toBoolean()
|
||||
// Use Expo CLI to bundle the app, this ensures the Metro config
|
||||
// works correctly with Expo projects.
|
||||
cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim())
|
||||
|
|
@ -64,9 +63,9 @@ react {
|
|||
}
|
||||
|
||||
/**
|
||||
* Set this to true in release builds to optimize the app using [R8](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization).
|
||||
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
|
||||
*/
|
||||
def enableMinifyInReleaseBuilds = (findProperty('android.enableMinifyInReleaseBuilds') ?: false).toBoolean()
|
||||
def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInReleaseBuilds') ?: false).toBoolean()
|
||||
|
||||
/**
|
||||
* The preferred build flavor of JavaScriptCore (JSC)
|
||||
|
|
@ -79,7 +78,9 @@ def enableMinifyInReleaseBuilds = (findProperty('android.enableMinifyInReleaseBu
|
|||
* give correct results when using with locales other than en-US. Note that
|
||||
* this variant is about 6MiB larger per architecture than default.
|
||||
*/
|
||||
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
|
||||
def jscFlavor = 'org.webkit:android-jsc:+'
|
||||
|
||||
apply from: new File(["node", "--print", "require('path').dirname(require.resolve('@sentry/react-native/package.json'))"].execute().text.trim(), "sentry.gradle")
|
||||
|
||||
android {
|
||||
ndkVersion rootProject.ext.ndkVersion
|
||||
|
|
@ -92,39 +93,16 @@ android {
|
|||
applicationId 'com.nuvio.app'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 38
|
||||
versionName "1.4.2"
|
||||
|
||||
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
|
||||
versionCode 14
|
||||
versionName "1.1.1"
|
||||
}
|
||||
|
||||
// Split APKs by architecture only for smaller downloads
|
||||
|
||||
splits {
|
||||
abi {
|
||||
enable true
|
||||
reset()
|
||||
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||
universalApk true
|
||||
}
|
||||
density {
|
||||
enable false
|
||||
}
|
||||
}
|
||||
|
||||
// Generate unique version codes for each split APK
|
||||
def abiVersionCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4]
|
||||
applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
def baseVersionCode = 38 // Current versionCode 38 from defaultConfig
|
||||
def abiName = output.getFilter(com.android.build.OutputFile.ABI)
|
||||
|
||||
def versionCode = baseVersionCode * 100 // Base multiplier
|
||||
|
||||
if (abiName != null) {
|
||||
versionCode += abiVersionCodes.get(abiName)
|
||||
}
|
||||
|
||||
output.versionCodeOverride = versionCode
|
||||
enable true
|
||||
universalApk false // If true, also generate a universal APK
|
||||
include "arm64-v8a", "armeabi-v7a", "x86", "x86_64"
|
||||
}
|
||||
}
|
||||
signingConfigs {
|
||||
|
|
@ -143,18 +121,15 @@ android {
|
|||
// Caution! In production, you need to generate your own keystore file.
|
||||
// see https://reactnative.dev/docs/signed-apk-android.
|
||||
signingConfig signingConfigs.debug
|
||||
def enableShrinkResources = findProperty('android.enableShrinkResourcesInReleaseBuilds') ?: 'false'
|
||||
shrinkResources enableShrinkResources.toBoolean()
|
||||
minifyEnabled enableMinifyInReleaseBuilds
|
||||
shrinkResources (findProperty('android.enableShrinkResourcesInReleaseBuilds')?.toBoolean() ?: false)
|
||||
minifyEnabled enableProguardInReleaseBuilds
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
def enablePngCrunchInRelease = findProperty('android.enablePngCrunchInReleaseBuilds') ?: 'true'
|
||||
crunchPngs enablePngCrunchInRelease.toBoolean()
|
||||
crunchPngs (findProperty('android.enablePngCrunchInReleaseBuilds')?.toBoolean() ?: true)
|
||||
}
|
||||
}
|
||||
packagingOptions {
|
||||
jniLibs {
|
||||
def enableLegacyPackaging = findProperty('expo.useLegacyPackaging') ?: 'false'
|
||||
useLegacyPackaging enableLegacyPackaging.toBoolean()
|
||||
useLegacyPackaging (findProperty('expo.useLegacyPackaging')?.toBoolean() ?: false)
|
||||
}
|
||||
}
|
||||
androidResources {
|
||||
|
|
@ -182,14 +157,7 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
exclude group: 'com.caverock', module: 'androidsvg'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// @generated begin react-native-google-cast-dependencies - expo prebuild (DO NOT MODIFY) sync-3822a3c86222e7aca74039b551612aab7e75365d
|
||||
implementation "com.google.android.gms:play-services-cast-framework:${safeExtGet('castFrameworkVersion', '+')}"
|
||||
// @generated end react-native-google-cast-dependencies
|
||||
// The version of react-native is set by the React Native Gradle Plugin
|
||||
implementation("com.facebook.react:react-android")
|
||||
|
||||
|
|
@ -199,15 +167,15 @@ dependencies {
|
|||
|
||||
if (isGifEnabled) {
|
||||
// For animated gif support
|
||||
implementation("com.facebook.fresco:animated-gif:${expoLibs.versions.fresco.get()}")
|
||||
implementation("com.facebook.fresco:animated-gif:${reactAndroidLibs.versions.fresco.get()}")
|
||||
}
|
||||
|
||||
if (isWebpEnabled) {
|
||||
// For webp support
|
||||
implementation("com.facebook.fresco:webpsupport:${expoLibs.versions.fresco.get()}")
|
||||
implementation("com.facebook.fresco:webpsupport:${reactAndroidLibs.versions.fresco.get()}")
|
||||
if (isWebpAnimatedEnabled) {
|
||||
// Animated webp support
|
||||
implementation("com.facebook.fresco:animated-webp:${expoLibs.versions.fresco.get()}")
|
||||
implementation("com.facebook.fresco:animated-webp:${reactAndroidLibs.versions.fresco.get()}")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -216,17 +184,4 @@ dependencies {
|
|||
} else {
|
||||
implementation jscFlavor
|
||||
}
|
||||
|
||||
// Include only FFmpeg decoder AAR to avoid duplicates with Maven Media3
|
||||
implementation files("libs/lib-decoder-ffmpeg-release.aar")
|
||||
|
||||
// MPV Player library
|
||||
implementation files("libs/libmpv-release.aar")
|
||||
|
||||
// Google Cast Framework
|
||||
implementation "com.google.android.gms:play-services-cast-framework:${safeExtGet('castFrameworkVersion', '+')}"
|
||||
}
|
||||
|
||||
def safeExtGet(prop, fallback) {
|
||||
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
||||
}
|
||||
|
|
|
|||
14
android/app/proguard-rules.pro
vendored
|
|
@ -12,17 +12,3 @@
|
|||
-keep class com.facebook.react.turbomodule.** { *; }
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# Media3 / ExoPlayer keep (extensions and reflection)
|
||||
-keep class androidx.media3.** { *; }
|
||||
-dontwarn androidx.media3.**
|
||||
|
||||
# FastImage / Glide ProGuard rules
|
||||
-keep public class com.dylanvann.fastimage.* {*;}
|
||||
-keep public class com.dylanvann.fastimage.** {*;}
|
||||
-keep public class * implements com.bumptech.glide.module.GlideModule
|
||||
-keep public class * extends com.bumptech.glide.module.AppGlideModule
|
||||
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
|
||||
**[] $VALUES;
|
||||
public *;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
|
||||
</manifest>
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
||||
<uses-sdk tools:overrideLibrary="dev.jdtech.mpv"/>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
|
|
@ -15,15 +13,13 @@
|
|||
<data android:scheme="https"/>
|
||||
</intent>
|
||||
</queries>
|
||||
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:enableOnBackInvokedCallback="false">
|
||||
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME" android:value="com.reactnative.googlecast.GoogleCastOptionsProvider"/>
|
||||
<meta-data android:name="com.reactnative.googlecast.RECEIVER_APPLICATION_ID" android:value="CC1AD845"/>
|
||||
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true">
|
||||
<meta-data android:name="expo.modules.updates.ENABLED" android:value="true"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_RUNTIME_VERSION" android:value="@string/expo_runtime_version"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ERROR_RECOVERY_ONLY"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="30000"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://ota.nuvioapp.space/api/manifest"/>
|
||||
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|uiMode|locale|layoutDirection" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="unspecified" android:supportsPictureInPicture="true" android:resizeableActivity="true">
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://grim-reyna-tapframe-69970143.koyeb.app/api/manifest"/>
|
||||
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode|locale|layoutDirection" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="unspecified">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
|
@ -32,8 +28,10 @@
|
|||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="nuvio"/>
|
||||
<data android:scheme="stremioexpo"/>
|
||||
<data android:scheme="com.nuvio.app"/>
|
||||
<data android:scheme="exp+nuvio"/>
|
||||
<data android:scheme="nuvio"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnable
|
|||
import com.facebook.react.defaults.DefaultReactActivityDelegate
|
||||
|
||||
import expo.modules.ReactActivityDelegateWrapper
|
||||
import com.reactnative.googlecast.api.RNGCCastContext
|
||||
|
||||
class MainActivity : ReactActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
|
@ -18,12 +17,6 @@ class MainActivity : ReactActivity() {
|
|||
// This is required for expo-splash-screen.
|
||||
setTheme(R.style.AppTheme);
|
||||
super.onCreate(null)
|
||||
// @generated begin react-native-google-cast-onCreate - expo prebuild (DO NOT MODIFY) sync-489050f2bf9933a98bbd9d93137016ae14c22faa
|
||||
RNGCCastContext.getSharedInstance(this)
|
||||
// @generated end react-native-google-cast-onCreate
|
||||
|
||||
// Initialize Google Cast context
|
||||
RNGCCastContext.getSharedInstance(this)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -5,34 +5,35 @@ import android.content.res.Configuration
|
|||
|
||||
import com.facebook.react.PackageList
|
||||
import com.facebook.react.ReactApplication
|
||||
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
|
||||
import com.facebook.react.ReactNativeHost
|
||||
import com.facebook.react.ReactPackage
|
||||
import com.facebook.react.ReactHost
|
||||
import com.facebook.react.common.ReleaseLevel
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
|
||||
import com.facebook.react.defaults.DefaultReactNativeHost
|
||||
import com.facebook.react.soloader.OpenSourceMergedSoMapping
|
||||
import com.facebook.soloader.SoLoader
|
||||
|
||||
import expo.modules.ApplicationLifecycleDispatcher
|
||||
import expo.modules.ReactNativeHostWrapper
|
||||
import com.nuvio.app.mpv.MpvPackage
|
||||
|
||||
class MainApplication : Application(), ReactApplication {
|
||||
|
||||
override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper(
|
||||
this,
|
||||
object : DefaultReactNativeHost(this) {
|
||||
override fun getPackages(): List<ReactPackage> =
|
||||
PackageList(this).packages.apply {
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
add(com.nuvio.app.mpv.MpvPackage())
|
||||
}
|
||||
this,
|
||||
object : DefaultReactNativeHost(this) {
|
||||
override fun getPackages(): List<ReactPackage> {
|
||||
val packages = PackageList(this).packages
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
// packages.add(new MyReactNativePackage());
|
||||
return packages
|
||||
}
|
||||
|
||||
override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry"
|
||||
|
||||
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
|
||||
|
||||
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
|
||||
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -41,12 +42,11 @@ class MainApplication : Application(), ReactApplication {
|
|||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
DefaultNewArchitectureEntryPoint.releaseLevel = try {
|
||||
ReleaseLevel.valueOf(BuildConfig.REACT_NATIVE_RELEASE_LEVEL.uppercase())
|
||||
} catch (e: IllegalArgumentException) {
|
||||
ReleaseLevel.STABLE
|
||||
SoLoader.init(this, OpenSourceMergedSoMapping)
|
||||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
||||
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
||||
load()
|
||||
}
|
||||
loadReactNative(this)
|
||||
ApplicationLifecycleDispatcher.onApplicationCreate(this)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,633 +0,0 @@
|
|||
package com.nuvio.app.mpv
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.SurfaceTexture
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.view.Surface
|
||||
import android.view.TextureView
|
||||
import dev.jdtech.mpv.MPVLib
|
||||
|
||||
import com.facebook.react.bridge.LifecycleEventListener
|
||||
import com.facebook.react.bridge.ReactContext
|
||||
|
||||
class MPVView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : TextureView(context, attrs, defStyleAttr), TextureView.SurfaceTextureListener, MPVLib.EventObserver {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "MPVView"
|
||||
}
|
||||
|
||||
private var isMpvInitialized = false
|
||||
private var pendingDataSource: String? = null
|
||||
private var isPaused: Boolean = true
|
||||
private var surface: Surface? = null
|
||||
private var httpHeaders: Map<String, String>? = null
|
||||
|
||||
// Decoder mode setting: 'auto', 'sw', 'hw', 'hw+' (default: auto)
|
||||
var decoderMode: String = "auto"
|
||||
|
||||
// GPU mode setting: 'gpu', 'gpu-next' (default: gpu)
|
||||
var gpuMode: String = "gpu"
|
||||
|
||||
// Flag to track if onLoad has been fired (prevents multiple fires for HLS streams)
|
||||
private var hasLoadEventFired: Boolean = false
|
||||
|
||||
// Event listener for React Native
|
||||
var onLoadCallback: ((duration: Double, width: Int, height: Int) -> Unit)? = null
|
||||
var onProgressCallback: ((position: Double, duration: Double) -> Unit)? = null
|
||||
var onEndCallback: (() -> Unit)? = null
|
||||
var onErrorCallback: ((message: String) -> Unit)? = null
|
||||
var onTracksChangedCallback: ((audioTracks: List<Map<String, Any>>, subtitleTracks: List<Map<String, Any>>) -> Unit)? = null
|
||||
|
||||
private var resumeOnForeground = false
|
||||
private val lifeCycleListener = object : LifecycleEventListener {
|
||||
override fun onHostPause() {
|
||||
resumeOnForeground = !isPaused;
|
||||
if(resumeOnForeground) {
|
||||
Log.d(TAG, "App backgrounded — pausing MPV")
|
||||
setPaused(true)
|
||||
}
|
||||
}
|
||||
override fun onHostResume() {
|
||||
if(resumeOnForeground) {
|
||||
setPaused(false)
|
||||
resumeOnForeground = false
|
||||
}
|
||||
}
|
||||
override fun onHostDestroy() {}
|
||||
}
|
||||
init {
|
||||
surfaceTextureListener = this
|
||||
isOpaque = false
|
||||
(context as? ReactContext)?.addLifecycleEventListener(lifeCycleListener)
|
||||
}
|
||||
|
||||
override fun onSurfaceTextureAvailable(surfaceTexture: SurfaceTexture, width: Int, height: Int) {
|
||||
Log.d(TAG, "Surface texture available: ${width}x${height}")
|
||||
try {
|
||||
surface = Surface(surfaceTexture)
|
||||
|
||||
MPVLib.create(context.applicationContext)
|
||||
initOptions()
|
||||
MPVLib.init()
|
||||
MPVLib.attachSurface(surface!!)
|
||||
MPVLib.addObserver(this)
|
||||
MPVLib.setPropertyString("android-surface-size", "${width}x${height}")
|
||||
observeProperties()
|
||||
isMpvInitialized = true
|
||||
|
||||
// If a data source was set before surface was ready, load it now
|
||||
// Headers are already applied in initOptions() before init()
|
||||
pendingDataSource?.let { url ->
|
||||
loadFile(url)
|
||||
pendingDataSource = null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to initialize MPV", e)
|
||||
onErrorCallback?.invoke("MPV initialization failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSurfaceTextureSizeChanged(surfaceTexture: SurfaceTexture, width: Int, height: Int) {
|
||||
Log.d(TAG, "Surface texture size changed: ${width}x${height}")
|
||||
if (isMpvInitialized) {
|
||||
MPVLib.setPropertyString("android-surface-size", "${width}x${height}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture): Boolean {
|
||||
Log.d(TAG, "Surface texture destroyed")
|
||||
(context as? ReactContext)?.removeLifecycleEventListener(lifeCycleListener)
|
||||
if (isMpvInitialized) {
|
||||
MPVLib.removeObserver(this)
|
||||
MPVLib.detachSurface()
|
||||
MPVLib.destroy()
|
||||
isMpvInitialized = false
|
||||
}
|
||||
surface?.release()
|
||||
surface = null
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onSurfaceTextureUpdated(surfaceTexture: SurfaceTexture) {
|
||||
// Called when the SurfaceTexture is updated via updateTexImage()
|
||||
}
|
||||
|
||||
private fun initOptions() {
|
||||
MPVLib.setOptionString("profile", "fast")
|
||||
|
||||
// GPU rendering mode (gpu or gpu-next)
|
||||
MPVLib.setOptionString("vo", gpuMode)
|
||||
MPVLib.setOptionString("gpu-context", "android")
|
||||
MPVLib.setOptionString("opengl-es", "yes")
|
||||
|
||||
// Decoder mode mapping (same as mpvKt)
|
||||
val hwdecValue = when (decoderMode) {
|
||||
"auto" -> "auto-copy" // Best balance: HW decode, copy to CPU for filters
|
||||
"sw" -> "no" // Software decoding only
|
||||
"hw" -> "mediacodec-copy" // HW decode with copy (safer)
|
||||
"hw+" -> "mediacodec" // Full HW decode (fastest, may have issues)
|
||||
else -> "auto-copy"
|
||||
}
|
||||
Log.d(TAG, "Decoder mode: $decoderMode, hwdec value: $hwdecValue, GPU mode: $gpuMode")
|
||||
MPVLib.setOptionString("hwdec", hwdecValue)
|
||||
// Note: Not setting hwdec-codecs explicitly - let mpv use defaults
|
||||
|
||||
MPVLib.setOptionString("target-colorspace-hint", "yes")
|
||||
|
||||
// HDR and Dolby Vision support
|
||||
// target-prim: Signal target display primaries (auto = passthrough when display supports)
|
||||
MPVLib.setOptionString("target-prim", "auto")
|
||||
// target-trc: Signal target transfer characteristics (auto = passthrough when display supports)
|
||||
MPVLib.setOptionString("target-trc", "auto")
|
||||
// tone-mapping: How to handle HDR/DV content on SDR displays (auto = best automatic choice)
|
||||
MPVLib.setOptionString("tone-mapping", "auto")
|
||||
// hdr-compute-peak: Compute peak brightness for better tone mapping
|
||||
MPVLib.setOptionString("hdr-compute-peak", "auto")
|
||||
// Allow DV Profile 5 (HEVC with RPU) to be decoded by hardware decoder
|
||||
MPVLib.setOptionString("vd-lavc-o", "strict=-2")
|
||||
|
||||
// Workaround for https://github.com/mpv-player/mpv/issues/14651
|
||||
MPVLib.setOptionString("vd-lavc-film-grain", "cpu")
|
||||
|
||||
MPVLib.setOptionString("ao", "audiotrack,opensles")
|
||||
|
||||
// Limit demuxer cache based on Android version (like mpvKt)
|
||||
val cacheMegs = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O_MR1) 64 else 32
|
||||
MPVLib.setOptionString("demuxer-max-bytes", "${cacheMegs * 1024 * 1024}")
|
||||
MPVLib.setOptionString("demuxer-max-back-bytes", "${cacheMegs * 1024 * 1024}")
|
||||
MPVLib.setOptionString("cache", "yes")
|
||||
MPVLib.setOptionString("cache-secs", "30")
|
||||
|
||||
MPVLib.setOptionString("network-timeout", "60")
|
||||
MPVLib.setOptionString("ytdl", "no")
|
||||
|
||||
applyHttpHeadersAsOptions()
|
||||
|
||||
MPVLib.setOptionString("tls-verify", "no")
|
||||
MPVLib.setOptionString("http-reconnect", "yes")
|
||||
MPVLib.setOptionString("stream-reconnect", "yes")
|
||||
|
||||
MPVLib.setOptionString("demuxer-lavf-o", "live_start_index=0,prefer_x_start=1,http_persistent=1")
|
||||
MPVLib.setOptionString("demuxer-seekable-cache", "yes")
|
||||
MPVLib.setOptionString("force-seekable", "yes")
|
||||
|
||||
MPVLib.setOptionString("sub-auto", "fuzzy")
|
||||
MPVLib.setOptionString("sub-visibility", "yes")
|
||||
MPVLib.setOptionString("sub-font-size", "48")
|
||||
MPVLib.setOptionString("sub-pos", "100")
|
||||
MPVLib.setOptionString("sub-color", "#FFFFFFFF")
|
||||
MPVLib.setOptionString("sub-border-size", "3")
|
||||
MPVLib.setOptionString("sub-border-color", "#FF000000")
|
||||
MPVLib.setOptionString("sub-shadow-offset", "2")
|
||||
MPVLib.setOptionString("sub-shadow-color", "#80000000")
|
||||
|
||||
MPVLib.setOptionString("osd-fonts-dir", "/system/fonts")
|
||||
MPVLib.setOptionString("sub-fonts-dir", "/system/fonts")
|
||||
MPVLib.setOptionString("sub-font", "Roboto")
|
||||
MPVLib.setOptionString("embeddedfonts", "yes")
|
||||
|
||||
MPVLib.setOptionString("sub-codepage", "auto")
|
||||
|
||||
MPVLib.setOptionString("blend-subtitles", "no")
|
||||
MPVLib.setOptionString("sub-use-margins", "yes")
|
||||
MPVLib.setOptionString("sub-ass-override", "force")
|
||||
MPVLib.setOptionString("sub-scale", "1.0")
|
||||
MPVLib.setOptionString("sub-fix-timing", "yes")
|
||||
|
||||
MPVLib.setOptionString("osc", "no")
|
||||
MPVLib.setOptionString("osd-level", "1")
|
||||
|
||||
MPVLib.setOptionString("sid", "auto")
|
||||
|
||||
MPVLib.setOptionString("terminal", "no")
|
||||
MPVLib.setOptionString("input-default-bindings", "no")
|
||||
}
|
||||
|
||||
private fun observeProperties() {
|
||||
// MPV format constants (from MPVLib source)
|
||||
val MPV_FORMAT_NONE = 0
|
||||
val MPV_FORMAT_FLAG = 3
|
||||
val MPV_FORMAT_INT64 = 4
|
||||
val MPV_FORMAT_DOUBLE = 5
|
||||
|
||||
MPVLib.observeProperty("time-pos", MPV_FORMAT_DOUBLE)
|
||||
MPVLib.observeProperty("duration/full", MPV_FORMAT_DOUBLE) // Use /full for complete HLS duration
|
||||
MPVLib.observeProperty("pause", MPV_FORMAT_FLAG)
|
||||
MPVLib.observeProperty("paused-for-cache", MPV_FORMAT_FLAG)
|
||||
MPVLib.observeProperty("eof-reached", MPV_FORMAT_FLAG)
|
||||
MPVLib.observeProperty("video-params/aspect", MPV_FORMAT_DOUBLE)
|
||||
MPVLib.observeProperty("width", MPV_FORMAT_INT64)
|
||||
MPVLib.observeProperty("height", MPV_FORMAT_INT64)
|
||||
MPVLib.observeProperty("track-list", MPV_FORMAT_NONE)
|
||||
|
||||
// Observe subtitle properties for debugging
|
||||
MPVLib.observeProperty("sid", MPV_FORMAT_INT64)
|
||||
MPVLib.observeProperty("sub-visibility", MPV_FORMAT_FLAG)
|
||||
MPVLib.observeProperty("sub-text", MPV_FORMAT_NONE)
|
||||
}
|
||||
|
||||
private fun loadFile(url: String) {
|
||||
Log.d(TAG, "Loading file: $url")
|
||||
// Reset load event flag for new file
|
||||
hasLoadEventFired = false
|
||||
|
||||
// Re-apply headers before loading to ensure segments/keys use the correct headers
|
||||
applyHttpHeadersAsOptions()
|
||||
|
||||
MPVLib.command(arrayOf("loadfile", url))
|
||||
}
|
||||
|
||||
// Public API
|
||||
|
||||
fun setDataSource(url: String) {
|
||||
if (isMpvInitialized) {
|
||||
// Headers were already set during initialization in initOptions()
|
||||
loadFile(url)
|
||||
} else {
|
||||
pendingDataSource = url
|
||||
}
|
||||
}
|
||||
|
||||
fun setHeaders(headers: Map<String, String>?) {
|
||||
httpHeaders = headers
|
||||
Log.d(TAG, "Headers set: $headers")
|
||||
if (isMpvInitialized) {
|
||||
applyHttpHeadersAsOptions()
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyHttpHeadersAsOptions() {
|
||||
// Find User-Agent (case-insensitive)
|
||||
val userAgentKey = httpHeaders?.keys?.find { it.equals("User-Agent", ignoreCase = true) }
|
||||
val userAgent = userAgentKey?.let { httpHeaders?.get(it) }
|
||||
?: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||
|
||||
Log.d(TAG, "Setting User-Agent: $userAgent")
|
||||
|
||||
if (isMpvInitialized) {
|
||||
MPVLib.setPropertyString("user-agent", userAgent)
|
||||
} else {
|
||||
MPVLib.setOptionString("user-agent", userAgent)
|
||||
}
|
||||
|
||||
httpHeaders?.let { headers ->
|
||||
val otherHeaders = headers.filterKeys { !it.equals("User-Agent", ignoreCase = true) }
|
||||
if (otherHeaders.isNotEmpty()) {
|
||||
// Use newline separator for http-header-fields as it's the standard for mpv
|
||||
val headerString = otherHeaders.map { (key, value) -> "$key: $value" }.joinToString("\n")
|
||||
Log.d(TAG, "Setting additional headers:\n$headerString")
|
||||
|
||||
if (isMpvInitialized) {
|
||||
MPVLib.setPropertyString("http-header-fields", headerString)
|
||||
} else {
|
||||
MPVLib.setOptionString("http-header-fields", headerString)
|
||||
}
|
||||
} else if (isMpvInitialized) {
|
||||
MPVLib.setPropertyString("http-header-fields", "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setPaused(paused: Boolean) {
|
||||
isPaused = paused
|
||||
if (isMpvInitialized) {
|
||||
MPVLib.setPropertyBoolean("pause", paused)
|
||||
}
|
||||
}
|
||||
|
||||
fun seekTo(positionSeconds: Double) {
|
||||
Log.d(TAG, "seekTo called: positionSeconds=$positionSeconds, isMpvInitialized=$isMpvInitialized")
|
||||
if (isMpvInitialized) {
|
||||
Log.d(TAG, "Executing MPV seek command: seek $positionSeconds absolute")
|
||||
MPVLib.command(arrayOf("seek", positionSeconds.toString(), "absolute"))
|
||||
}
|
||||
}
|
||||
|
||||
fun setSpeed(speed: Double) {
|
||||
if (isMpvInitialized) {
|
||||
MPVLib.setPropertyDouble("speed", speed)
|
||||
}
|
||||
}
|
||||
|
||||
fun setVolume(volume: Double) {
|
||||
if (isMpvInitialized) {
|
||||
// MPV volume is 0-100
|
||||
MPVLib.setPropertyDouble("volume", volume * 100.0)
|
||||
}
|
||||
}
|
||||
|
||||
fun setAudioTrack(trackId: Int) {
|
||||
if (isMpvInitialized) {
|
||||
if (trackId == -1) {
|
||||
MPVLib.setPropertyString("aid", "no")
|
||||
} else {
|
||||
MPVLib.setPropertyInt("aid", trackId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setSubtitleTrack(trackId: Int) {
|
||||
Log.d(TAG, "setSubtitleTrack called: trackId=$trackId, isMpvInitialized=$isMpvInitialized")
|
||||
if (isMpvInitialized) {
|
||||
if (trackId == -1) {
|
||||
Log.d(TAG, "Disabling subtitles (sid=no)")
|
||||
MPVLib.setPropertyString("sid", "no")
|
||||
MPVLib.setPropertyString("sub-visibility", "no")
|
||||
} else {
|
||||
Log.d(TAG, "Setting subtitle track to: $trackId")
|
||||
MPVLib.setPropertyInt("sid", trackId)
|
||||
// Ensure subtitles are visible
|
||||
MPVLib.setPropertyString("sub-visibility", "yes")
|
||||
|
||||
// Debug: Verify the subtitle was set correctly
|
||||
val currentSid = MPVLib.getPropertyInt("sid")
|
||||
val subVisibility = MPVLib.getPropertyString("sub-visibility")
|
||||
val subDelay = MPVLib.getPropertyDouble("sub-delay")
|
||||
val subScale = MPVLib.getPropertyDouble("sub-scale")
|
||||
Log.d(TAG, "After setting - sid=$currentSid, sub-visibility=$subVisibility, sub-delay=$subDelay, sub-scale=$subScale")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setResizeMode(mode: String) {
|
||||
Log.d(TAG, "setResizeMode called: mode=$mode, isMpvInitialized=$isMpvInitialized")
|
||||
if (isMpvInitialized) {
|
||||
when (mode) {
|
||||
"contain" -> {
|
||||
// Letterbox - show entire video with black bars
|
||||
MPVLib.setPropertyDouble("panscan", 0.0)
|
||||
MPVLib.setPropertyString("keepaspect", "yes")
|
||||
}
|
||||
"cover" -> {
|
||||
// Fill/crop - zoom to fill, cropping edges
|
||||
MPVLib.setPropertyDouble("panscan", 1.0)
|
||||
MPVLib.setPropertyString("keepaspect", "yes")
|
||||
}
|
||||
"stretch" -> {
|
||||
// Stretch - disable aspect ratio
|
||||
MPVLib.setPropertyDouble("panscan", 0.0)
|
||||
MPVLib.setPropertyString("keepaspect", "no")
|
||||
}
|
||||
else -> {
|
||||
// Default to contain
|
||||
MPVLib.setPropertyDouble("panscan", 0.0)
|
||||
MPVLib.setPropertyString("keepaspect", "yes")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Subtitle Styling Methods
|
||||
|
||||
fun setSubtitleSize(size: Int) {
|
||||
if (isMpvInitialized) {
|
||||
Log.d(TAG, "Setting subtitle size: $size")
|
||||
MPVLib.setPropertyInt("sub-font-size", size)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSubtitleColor(color: String) {
|
||||
if (isMpvInitialized) {
|
||||
// MPV expects color in #AARRGGBB format, but we receive #RRGGBB
|
||||
// Convert to MPV format with full opacity
|
||||
val mpvColor = if (color.length == 7) "#FF${color.substring(1)}" else color
|
||||
Log.d(TAG, "Setting subtitle color: $mpvColor")
|
||||
MPVLib.setPropertyString("sub-color", mpvColor)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSubtitleBackgroundColor(color: String, opacity: Float) {
|
||||
if (isMpvInitialized) {
|
||||
// Convert opacity (0-1) to hex (00-FF)
|
||||
val alphaHex = (opacity * 255).toInt().coerceIn(0, 255).let {
|
||||
String.format("%02X", it)
|
||||
}
|
||||
// MPV format: #AARRGGBB
|
||||
val baseColor = if (color.startsWith("#")) color.substring(1) else color
|
||||
val mpvColor = "#${alphaHex}${baseColor.takeLast(6)}"
|
||||
Log.d(TAG, "Setting subtitle background: $mpvColor (opacity: $opacity)")
|
||||
MPVLib.setPropertyString("sub-back-color", mpvColor)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSubtitleBorderSize(size: Int) {
|
||||
if (isMpvInitialized) {
|
||||
Log.d(TAG, "Setting subtitle border size: $size")
|
||||
MPVLib.setPropertyInt("sub-border-size", size)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSubtitleBorderColor(color: String) {
|
||||
if (isMpvInitialized) {
|
||||
val mpvColor = if (color.length == 7) "#FF${color.substring(1)}" else color
|
||||
Log.d(TAG, "Setting subtitle border color: $mpvColor")
|
||||
MPVLib.setPropertyString("sub-border-color", mpvColor)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSubtitleShadow(enabled: Boolean, offset: Int) {
|
||||
if (isMpvInitialized) {
|
||||
Log.d(TAG, "Setting subtitle shadow: enabled=$enabled, offset=$offset")
|
||||
if (enabled) {
|
||||
MPVLib.setPropertyInt("sub-shadow-offset", offset)
|
||||
MPVLib.setPropertyString("sub-shadow-color", "#80000000")
|
||||
} else {
|
||||
MPVLib.setPropertyInt("sub-shadow-offset", 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setSubtitlePosition(pos: Int) {
|
||||
if (isMpvInitialized) {
|
||||
// sub-pos: 0=top, 100=bottom, can go beyond 100 for more offset
|
||||
// UI sends bottomOffset (0=at bottom, higher=more up from bottom)
|
||||
// Convert: MPV pos = 100 - (bottomOffset / screenHeightFactor)
|
||||
// Simplified: just pass pos directly, UI should convert
|
||||
Log.d(TAG, "Setting subtitle position: $pos")
|
||||
MPVLib.setPropertyInt("sub-pos", pos)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSubtitleDelay(delaySec: Double) {
|
||||
if (isMpvInitialized) {
|
||||
Log.d(TAG, "Setting subtitle delay: $delaySec seconds")
|
||||
MPVLib.setPropertyDouble("sub-delay", delaySec)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSubtitleScale(scale: Double) {
|
||||
if (isMpvInitialized) {
|
||||
Log.d(TAG, "Setting subtitle scale: $scale")
|
||||
MPVLib.setPropertyDouble("sub-scale", scale)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSubtitleAlignment(align: String) {
|
||||
if (isMpvInitialized) {
|
||||
// MPV sub-justify values: left, center, right, auto
|
||||
val mpvAlign = when (align) {
|
||||
"left" -> "left"
|
||||
"right" -> "right"
|
||||
"center" -> "center"
|
||||
else -> "center"
|
||||
}
|
||||
Log.d(TAG, "Setting subtitle alignment: $mpvAlign")
|
||||
MPVLib.setPropertyString("sub-justify", mpvAlign)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSubtitleBold(bold: Boolean) {
|
||||
if (isMpvInitialized) {
|
||||
Log.d(TAG, "Setting subtitle bold: $bold")
|
||||
MPVLib.setPropertyString("sub-bold", if (bold) "yes" else "no")
|
||||
}
|
||||
}
|
||||
|
||||
fun setSubtitleItalic(italic: Boolean) {
|
||||
if (isMpvInitialized) {
|
||||
Log.d(TAG, "Setting subtitle italic: $italic")
|
||||
MPVLib.setPropertyString("sub-italic", if (italic) "yes" else "no")
|
||||
}
|
||||
}
|
||||
|
||||
// MPVLib.EventObserver implementation
|
||||
|
||||
override fun eventProperty(property: String) {
|
||||
Log.d(TAG, "Property changed: $property")
|
||||
when (property) {
|
||||
"track-list" -> {
|
||||
// Parse track list and notify React Native
|
||||
parseAndSendTracks()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseAndSendTracks() {
|
||||
try {
|
||||
val trackCount = MPVLib.getPropertyInt("track-list/count") ?: 0
|
||||
Log.d(TAG, "Track count: $trackCount")
|
||||
|
||||
val audioTracks = mutableListOf<Map<String, Any>>()
|
||||
val subtitleTracks = mutableListOf<Map<String, Any>>()
|
||||
|
||||
for (i in 0 until trackCount) {
|
||||
val type = MPVLib.getPropertyString("track-list/$i/type") ?: continue
|
||||
val id = MPVLib.getPropertyInt("track-list/$i/id") ?: continue
|
||||
val title = MPVLib.getPropertyString("track-list/$i/title") ?: ""
|
||||
val lang = MPVLib.getPropertyString("track-list/$i/lang") ?: ""
|
||||
val codec = MPVLib.getPropertyString("track-list/$i/codec") ?: ""
|
||||
|
||||
val trackName = when {
|
||||
title.isNotEmpty() -> title
|
||||
lang.isNotEmpty() -> lang.uppercase()
|
||||
else -> "Track $id"
|
||||
}
|
||||
|
||||
val track = mapOf(
|
||||
"id" to id,
|
||||
"name" to trackName,
|
||||
"language" to lang,
|
||||
"codec" to codec
|
||||
)
|
||||
|
||||
when (type) {
|
||||
"audio" -> {
|
||||
Log.d(TAG, "Found audio track: $track")
|
||||
audioTracks.add(track)
|
||||
}
|
||||
"sub" -> {
|
||||
Log.d(TAG, "Found subtitle track: $track")
|
||||
subtitleTracks.add(track)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "Sending tracks - Audio: ${audioTracks.size}, Subtitles: ${subtitleTracks.size}")
|
||||
onTracksChangedCallback?.invoke(audioTracks, subtitleTracks)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error parsing tracks", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun eventProperty(property: String, value: Long) {
|
||||
Log.d(TAG, "Property $property = $value (Long)")
|
||||
}
|
||||
|
||||
override fun eventProperty(property: String, value: Double) {
|
||||
Log.d(TAG, "Property $property = $value (Double)")
|
||||
when (property) {
|
||||
"time-pos" -> {
|
||||
val duration = MPVLib.getPropertyDouble("duration/full") ?: MPVLib.getPropertyDouble("duration") ?: 0.0
|
||||
onProgressCallback?.invoke(value, duration)
|
||||
}
|
||||
"duration/full", "duration" -> {
|
||||
// Only fire onLoad once when video dimensions are available
|
||||
// For HLS streams, duration updates incrementally as segments are fetched
|
||||
if (!hasLoadEventFired) {
|
||||
val width = MPVLib.getPropertyInt("width") ?: 0
|
||||
val height = MPVLib.getPropertyInt("height") ?: 0
|
||||
// Wait until we have valid dimensions before firing onLoad
|
||||
if (width > 0 && height > 0 && value > 0) {
|
||||
hasLoadEventFired = true
|
||||
Log.d(TAG, "Firing onLoad event: duration=$value, width=$width, height=$height")
|
||||
onLoadCallback?.invoke(value, width, height)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun eventProperty(property: String, value: Boolean) {
|
||||
Log.d(TAG, "Property $property = $value (Boolean)")
|
||||
when (property) {
|
||||
"eof-reached" -> {
|
||||
if (value) {
|
||||
onEndCallback?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun eventProperty(property: String, value: String) {
|
||||
Log.d(TAG, "Property $property = $value (String)")
|
||||
}
|
||||
|
||||
override fun event(eventId: Int) {
|
||||
Log.d(TAG, "Event: $eventId")
|
||||
// MPV event constants (from MPVLib source)
|
||||
val MPV_EVENT_FILE_LOADED = 8
|
||||
val MPV_EVENT_END_FILE = 7
|
||||
|
||||
when (eventId) {
|
||||
MPV_EVENT_FILE_LOADED -> {
|
||||
// File is loaded, start playback if not paused
|
||||
if (!isPaused) {
|
||||
MPVLib.setPropertyBoolean("pause", false)
|
||||
}
|
||||
}
|
||||
MPV_EVENT_END_FILE -> {
|
||||
Log.d(TAG, "MPV_EVENT_END_FILE")
|
||||
|
||||
// Heuristic: If duration is effectively 0 at end of file, it's a load error
|
||||
val duration = MPVLib.getPropertyDouble("duration/full") ?: MPVLib.getPropertyDouble("duration") ?: 0.0
|
||||
val timePos = MPVLib.getPropertyDouble("time-pos") ?: 0.0
|
||||
val eofReached = MPVLib.getPropertyBoolean("eof-reached") ?: false
|
||||
|
||||
Log.d(TAG, "End stats - Duration: $duration, Time: $timePos, EOF: $eofReached")
|
||||
|
||||
if (duration < 1.0 && !eofReached) {
|
||||
val customError = "Unable to play media. Source may be unreachable."
|
||||
Log.e(TAG, "Playback error detected (heuristic): $customError")
|
||||
onErrorCallback?.invoke(customError)
|
||||
} else {
|
||||
onEndCallback?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
package com.nuvio.app.mpv
|
||||
|
||||
import com.facebook.react.ReactPackage
|
||||
import com.facebook.react.bridge.NativeModule
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.uimanager.ViewManager
|
||||
|
||||
class MpvPackage : ReactPackage {
|
||||
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
||||
return listOf(MpvPlayerViewManager(reactContext))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,241 +0,0 @@
|
|||
package com.nuvio.app.mpv
|
||||
|
||||
import android.graphics.Color
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.common.MapBuilder
|
||||
import com.facebook.react.uimanager.SimpleViewManager
|
||||
import com.facebook.react.uimanager.ThemedReactContext
|
||||
import com.facebook.react.uimanager.annotations.ReactProp
|
||||
import com.facebook.react.uimanager.events.RCTEventEmitter
|
||||
|
||||
class MpvPlayerViewManager(
|
||||
private val reactContext: ReactApplicationContext
|
||||
) : SimpleViewManager<MPVView>() {
|
||||
|
||||
companion object {
|
||||
const val REACT_CLASS = "MpvPlayer"
|
||||
|
||||
// Commands
|
||||
const val COMMAND_SEEK = 1
|
||||
const val COMMAND_SET_AUDIO_TRACK = 2
|
||||
const val COMMAND_SET_SUBTITLE_TRACK = 3
|
||||
}
|
||||
|
||||
override fun getName(): String = REACT_CLASS
|
||||
|
||||
override fun createViewInstance(context: ThemedReactContext): MPVView {
|
||||
val view = MPVView(context)
|
||||
// Note: Do NOT set background color - it will block the SurfaceView content
|
||||
|
||||
// Set up event callbacks
|
||||
view.onLoadCallback = { duration, width, height ->
|
||||
val event = Arguments.createMap().apply {
|
||||
putDouble("duration", duration)
|
||||
putInt("width", width)
|
||||
putInt("height", height)
|
||||
}
|
||||
sendEvent(context, view.id, "onLoad", event)
|
||||
}
|
||||
|
||||
view.onProgressCallback = { position, duration ->
|
||||
val event = Arguments.createMap().apply {
|
||||
putDouble("currentTime", position)
|
||||
putDouble("duration", duration)
|
||||
}
|
||||
sendEvent(context, view.id, "onProgress", event)
|
||||
}
|
||||
|
||||
view.onEndCallback = {
|
||||
sendEvent(context, view.id, "onEnd", Arguments.createMap())
|
||||
}
|
||||
|
||||
view.onErrorCallback = { message ->
|
||||
val event = Arguments.createMap().apply {
|
||||
putString("error", message)
|
||||
}
|
||||
sendEvent(context, view.id, "onError", event)
|
||||
}
|
||||
|
||||
view.onTracksChangedCallback = { audioTracks, subtitleTracks ->
|
||||
val event = Arguments.createMap().apply {
|
||||
val audioArray = Arguments.createArray()
|
||||
audioTracks.forEach { track ->
|
||||
val trackMap = Arguments.createMap().apply {
|
||||
putInt("id", track["id"] as Int)
|
||||
putString("name", track["name"] as String)
|
||||
putString("language", track["language"] as String)
|
||||
putString("codec", track["codec"] as String)
|
||||
}
|
||||
audioArray.pushMap(trackMap)
|
||||
}
|
||||
putArray("audioTracks", audioArray)
|
||||
|
||||
val subtitleArray = Arguments.createArray()
|
||||
subtitleTracks.forEach { track ->
|
||||
val trackMap = Arguments.createMap().apply {
|
||||
putInt("id", track["id"] as Int)
|
||||
putString("name", track["name"] as String)
|
||||
putString("language", track["language"] as String)
|
||||
putString("codec", track["codec"] as String)
|
||||
}
|
||||
subtitleArray.pushMap(trackMap)
|
||||
}
|
||||
putArray("subtitleTracks", subtitleArray)
|
||||
}
|
||||
sendEvent(context, view.id, "onTracksChanged", event)
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
private fun sendEvent(context: ThemedReactContext, viewId: Int, eventName: String, params: com.facebook.react.bridge.WritableMap) {
|
||||
context.getJSModule(RCTEventEmitter::class.java)
|
||||
.receiveEvent(viewId, eventName, params)
|
||||
}
|
||||
|
||||
override fun getExportedCustomBubblingEventTypeConstants(): Map<String, Any> {
|
||||
return MapBuilder.builder<String, Any>()
|
||||
.put("onLoad", MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "onLoad")))
|
||||
.put("onProgress", MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "onProgress")))
|
||||
.put("onEnd", MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "onEnd")))
|
||||
.put("onError", MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "onError")))
|
||||
.put("onTracksChanged", MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "onTracksChanged")))
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun getCommandsMap(): Map<String, Int> {
|
||||
return MapBuilder.of(
|
||||
"seek", COMMAND_SEEK,
|
||||
"setAudioTrack", COMMAND_SET_AUDIO_TRACK,
|
||||
"setSubtitleTrack", COMMAND_SET_SUBTITLE_TRACK
|
||||
)
|
||||
}
|
||||
|
||||
override fun receiveCommand(view: MPVView, commandId: String?, args: ReadableArray?) {
|
||||
android.util.Log.d("MpvPlayerViewManager", "receiveCommand: $commandId, args: $args")
|
||||
when (commandId) {
|
||||
"seek" -> {
|
||||
val position = args?.getDouble(0)
|
||||
android.util.Log.d("MpvPlayerViewManager", "Seek command received: position=$position")
|
||||
position?.let { view.seekTo(it) }
|
||||
}
|
||||
"setAudioTrack" -> {
|
||||
args?.getInt(0)?.let { view.setAudioTrack(it) }
|
||||
}
|
||||
"setSubtitleTrack" -> {
|
||||
args?.getInt(0)?.let { view.setSubtitleTrack(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// React Props
|
||||
|
||||
@ReactProp(name = "source")
|
||||
fun setSource(view: MPVView, source: String?) {
|
||||
source?.let { view.setDataSource(it) }
|
||||
}
|
||||
|
||||
@ReactProp(name = "paused")
|
||||
fun setPaused(view: MPVView, paused: Boolean) {
|
||||
view.setPaused(paused)
|
||||
}
|
||||
|
||||
@ReactProp(name = "volume", defaultFloat = 1.0f)
|
||||
fun setVolume(view: MPVView, volume: Float) {
|
||||
view.setVolume(volume.toDouble())
|
||||
}
|
||||
|
||||
@ReactProp(name = "rate", defaultFloat = 1.0f)
|
||||
fun setRate(view: MPVView, rate: Float) {
|
||||
view.setSpeed(rate.toDouble())
|
||||
}
|
||||
|
||||
// Handle backgroundColor prop to prevent crash from React Native style system
|
||||
@ReactProp(name = "backgroundColor", customType = "Color")
|
||||
fun setBackgroundColor(view: MPVView, color: Int?) {
|
||||
// Intentionally ignoring - background color would block the TextureView content
|
||||
// Leave the view transparent
|
||||
}
|
||||
|
||||
@ReactProp(name = "resizeMode")
|
||||
fun setResizeMode(view: MPVView, resizeMode: String?) {
|
||||
view.setResizeMode(resizeMode ?: "contain")
|
||||
}
|
||||
|
||||
@ReactProp(name = "headers")
|
||||
fun setHeaders(view: MPVView, headers: com.facebook.react.bridge.ReadableMap?) {
|
||||
if (headers != null) {
|
||||
val headerMap = mutableMapOf<String, String>()
|
||||
val iterator = headers.keySetIterator()
|
||||
while (iterator.hasNextKey()) {
|
||||
val key = iterator.nextKey()
|
||||
headers.getString(key)?.let { value ->
|
||||
headerMap[key] = value
|
||||
}
|
||||
}
|
||||
view.setHeaders(headerMap)
|
||||
} else {
|
||||
view.setHeaders(null)
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = "decoderMode")
|
||||
fun setDecoderMode(view: MPVView, decoderMode: String?) {
|
||||
view.decoderMode = decoderMode ?: "auto"
|
||||
}
|
||||
|
||||
@ReactProp(name = "gpuMode")
|
||||
fun setGpuMode(view: MPVView, gpuMode: String?) {
|
||||
view.gpuMode = gpuMode ?: "gpu"
|
||||
}
|
||||
|
||||
// Subtitle Styling Props
|
||||
|
||||
@ReactProp(name = "subtitleSize", defaultInt = 48)
|
||||
fun setSubtitleSize(view: MPVView, size: Int) {
|
||||
view.setSubtitleSize(size)
|
||||
}
|
||||
|
||||
@ReactProp(name = "subtitleColor")
|
||||
fun setSubtitleColor(view: MPVView, color: String?) {
|
||||
view.setSubtitleColor(color ?: "#FFFFFF")
|
||||
}
|
||||
|
||||
@ReactProp(name = "subtitleBackgroundOpacity", defaultFloat = 0.0f)
|
||||
fun setSubtitleBackgroundOpacity(view: MPVView, opacity: Float) {
|
||||
// Black background with user-specified opacity
|
||||
view.setSubtitleBackgroundColor("#000000", opacity)
|
||||
}
|
||||
|
||||
@ReactProp(name = "subtitleBorderSize", defaultInt = 3)
|
||||
fun setSubtitleBorderSize(view: MPVView, size: Int) {
|
||||
view.setSubtitleBorderSize(size)
|
||||
}
|
||||
|
||||
@ReactProp(name = "subtitleBorderColor")
|
||||
fun setSubtitleBorderColor(view: MPVView, color: String?) {
|
||||
view.setSubtitleBorderColor(color ?: "#000000")
|
||||
}
|
||||
|
||||
@ReactProp(name = "subtitleShadowEnabled", defaultBoolean = true)
|
||||
fun setSubtitleShadowEnabled(view: MPVView, enabled: Boolean) {
|
||||
view.setSubtitleShadow(enabled, if (enabled) 2 else 0)
|
||||
}
|
||||
|
||||
@ReactProp(name = "subtitlePosition", defaultInt = 100)
|
||||
fun setSubtitlePosition(view: MPVView, pos: Int) {
|
||||
view.setSubtitlePosition(pos)
|
||||
}
|
||||
|
||||
@ReactProp(name = "subtitleDelay", defaultFloat = 0.0f)
|
||||
fun setSubtitleDelay(view: MPVView, delay: Float) {
|
||||
view.setSubtitleDelay(delay.toDouble())
|
||||
}
|
||||
|
||||
@ReactProp(name = "subtitleAlignment")
|
||||
fun setSubtitleAlignment(view: MPVView, align: String?) {
|
||||
view.setSubtitleAlignment(align ?: "center")
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 3 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 7 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 27 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.4.2</string>
|
||||
<string name="expo_runtime_version">1.1.1</string>
|
||||
</resources>
|
||||
|
|
@ -1,11 +1,17 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<style name="AppTheme" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<item name="android:enforceNavigationBarContrast" tools:targetApi="29">true</item>
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<item name="android:textColor">@android:color/black</item>
|
||||
<item name="android:editTextStyle">@style/ResetEditText</item>
|
||||
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="android:statusBarColor">#020404</item>
|
||||
<item name="android:windowBackground">@color/activityBackground</item>
|
||||
</style>
|
||||
<style name="ResetEditText" parent="@android:style/Widget.EditText">
|
||||
<item name="android:padding">0dp</item>
|
||||
<item name="android:textColorHint">#c8c8c8</item>
|
||||
<item name="android:textColor">@android:color/black</item>
|
||||
</style>
|
||||
<style name="Theme.App.SplashScreen" parent="AppTheme">
|
||||
<item name="android:windowBackground">@drawable/ic_launcher_background</item>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,32 +1,41 @@
|
|||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "35.0.0"
|
||||
minSdkVersion = 24
|
||||
compileSdkVersion = 36
|
||||
targetSdkVersion = 35
|
||||
castFrameworkVersion = "22.1.0"
|
||||
ndkVersion = "29.0.14206865" // Required for libmpv AAR built with NDK r29
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath('com.android.tools.build:gradle')
|
||||
classpath('com.facebook.react:react-native-gradle-plugin')
|
||||
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
|
||||
}
|
||||
ext {
|
||||
buildToolsVersion = findProperty('android.buildToolsVersion') ?: '35.0.0'
|
||||
minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '24')
|
||||
compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '35')
|
||||
targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '34')
|
||||
kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.25'
|
||||
|
||||
ndkVersion = "26.1.10909125"
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath('com.android.tools.build:gradle')
|
||||
classpath('com.facebook.react:react-native-gradle-plugin')
|
||||
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: "com.facebook.react.rootproject"
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
}
|
||||
}
|
||||
repositories {
|
||||
maven {
|
||||
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
|
||||
url(new File(['node', '--print', "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), '../android'))
|
||||
}
|
||||
maven {
|
||||
// Android JSC is installed from npm
|
||||
url(new File(['node', '--print', "require.resolve('jsc-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), '../dist'))
|
||||
}
|
||||
|
||||
apply plugin: "expo-root-project"
|
||||
apply plugin: "com.facebook.react.rootproject"
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,15 +10,12 @@
|
|||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
|
||||
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m
|
||||
|
||||
# Enable Gradle Build Cache
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
org.gradle.parallel=true
|
||||
# org.gradle.parallel=true
|
||||
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
|
|
@ -44,11 +41,6 @@ newArchEnabled=true
|
|||
# If set to false, you will be using JSC instead.
|
||||
hermesEnabled=true
|
||||
|
||||
# Use this property to enable edge-to-edge display support.
|
||||
# This allows your app to draw behind system bars for an immersive UI.
|
||||
# Note: Only works with ReactActivity and should not be used with custom Activity.
|
||||
edgeToEdgeEnabled=true
|
||||
|
||||
# Enable GIF support in React Native images (~200 B increase)
|
||||
expo.gif.enabled=true
|
||||
# Enable webp support in React Native images (~85 KB increase)
|
||||
|
|
@ -62,7 +54,3 @@ EX_DEV_CLIENT_NETWORK_INSPECTOR=true
|
|||
|
||||
# Use legacy packaging to compress native libraries in the resulting APK.
|
||||
expo.useLegacyPackaging=false
|
||||
|
||||
# Specifies whether the app is configured to use edge-to-edge via the app config or plugin
|
||||
# WARNING: This property has been deprecated and will be removed in Expo SDK 55. Use `edgeToEdgeEnabled` or `react.edgeToEdgeEnabled` to determine whether the project is using edge-to-edge.
|
||||
expo.edgeToEdgeEnabled=true
|
||||
|
|
|
|||
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
|
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
|||
7
android/gradlew
vendored
Executable file → Normal file
|
|
@ -86,7 +86,8 @@ done
|
|||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||
' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
|
@ -114,7 +115,7 @@ case "$( uname )" in #(
|
|||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH="\\\"\\\""
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
|
|
@ -213,7 +214,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
|||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
|
|
|
|||
4
android/gradlew.bat
vendored
|
|
@ -70,11 +70,11 @@ goto fail
|
|||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|
|
|
|||
|
|
@ -1,39 +1,38 @@
|
|||
pluginManagement {
|
||||
def reactNativeGradlePlugin = new File(
|
||||
providers.exec {
|
||||
workingDir(rootDir)
|
||||
commandLine("node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })")
|
||||
}.standardOutput.asText.get().trim()
|
||||
).getParentFile().absolutePath
|
||||
includeBuild(reactNativeGradlePlugin)
|
||||
|
||||
def expoPluginsPath = new File(
|
||||
providers.exec {
|
||||
workingDir(rootDir)
|
||||
commandLine("node", "--print", "require.resolve('expo-modules-autolinking/package.json', { paths: [require.resolve('expo/package.json')] })")
|
||||
}.standardOutput.asText.get().trim(),
|
||||
"../android/expo-gradle-plugin"
|
||||
).absolutePath
|
||||
includeBuild(expoPluginsPath)
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("com.facebook.react.settings")
|
||||
id("expo-autolinking-settings")
|
||||
includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().toString())
|
||||
}
|
||||
plugins { id("com.facebook.react.settings") }
|
||||
|
||||
extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
|
||||
if (System.getenv('EXPO_USE_COMMUNITY_AUTOLINKING') == '1') {
|
||||
ex.autolinkLibrariesFromCommand()
|
||||
} else {
|
||||
ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand)
|
||||
def command = [
|
||||
'node',
|
||||
'--no-warnings',
|
||||
'--eval',
|
||||
'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))',
|
||||
'react-native-config',
|
||||
'--json',
|
||||
'--platform',
|
||||
'android'
|
||||
].toList()
|
||||
ex.autolinkLibrariesFromCommand(command)
|
||||
}
|
||||
}
|
||||
expoAutolinking.useExpoModules()
|
||||
|
||||
rootProject.name = 'Nuvio'
|
||||
|
||||
expoAutolinking.useExpoVersionCatalog()
|
||||
dependencyResolutionManagement {
|
||||
versionCatalogs {
|
||||
reactAndroidLibs {
|
||||
from(files(new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), "../gradle/libs.versions.toml")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle");
|
||||
useExpoModules()
|
||||
|
||||
include ':app'
|
||||
includeBuild(expoAutolinking.reactNativeGradlePlugin)
|
||||
includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile())
|
||||
|
|
|
|||
52
app.json
|
|
@ -2,7 +2,7 @@
|
|||
"expo": {
|
||||
"name": "Nuvio",
|
||||
"slug": "nuvio",
|
||||
"version": "1.4.2",
|
||||
"version": "1.1.1",
|
||||
"orientation": "default",
|
||||
"backgroundColor": "#020404",
|
||||
"icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png",
|
||||
|
|
@ -10,43 +10,32 @@
|
|||
"scheme": "nuvio",
|
||||
"newArchEnabled": true,
|
||||
"splash": {
|
||||
"image": "./src/assets/splash-icon-new.png",
|
||||
"image": "./assets/splash-icon.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#020404"
|
||||
},
|
||||
"ios": {
|
||||
"supportsTablet": true,
|
||||
"icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png",
|
||||
"buildNumber": "38",
|
||||
"buildNumber": "14",
|
||||
"infoPlist": {
|
||||
"NSAppTransportSecurity": {
|
||||
"NSAllowsArbitraryLoads": true
|
||||
},
|
||||
"NSBonjourServices": [
|
||||
"_http._tcp",
|
||||
"_googlecast._tcp",
|
||||
"_CC1AD845._googlecast._tcp"
|
||||
"_http._tcp"
|
||||
],
|
||||
"NSLocalNetworkUsageDescription": "Nuvio uses the local network to discover Cast-enabled devices on your WiFi network and to connect to local media servers.",
|
||||
"NSLocalNetworkUsageDescription": "App uses the local network to discover and connect to devices.",
|
||||
"NSMicrophoneUsageDescription": "This app does not require microphone access.",
|
||||
"UIBackgroundModes": [
|
||||
"audio"
|
||||
],
|
||||
"LSSupportsOpeningDocumentsInPlace": true,
|
||||
"UIFileSharingEnabled": true,
|
||||
"LSApplicationQueriesSchemes": [
|
||||
"vlc",
|
||||
"vlc-x-callback",
|
||||
"infuse",
|
||||
"outplayer",
|
||||
"open-vidhub",
|
||||
"livecontainer"
|
||||
]
|
||||
"UIFileSharingEnabled": true
|
||||
},
|
||||
"bundleIdentifier": "com.nuvio.hub",
|
||||
"bundleIdentifier": "com.nuvio.app",
|
||||
"associatedDomains": [],
|
||||
"jsEngine": "hermes",
|
||||
"appleTeamId": "8QBDZ766S3"
|
||||
"jsEngine": "hermes"
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
|
|
@ -56,11 +45,10 @@
|
|||
"icon": "./assets/android/mipmap-xxxhdpi/ic_launcher.png",
|
||||
"permissions": [
|
||||
"INTERNET",
|
||||
"WAKE_LOCK",
|
||||
"android.permission.WRITE_SETTINGS"
|
||||
"WAKE_LOCK"
|
||||
],
|
||||
"package": "com.nuvio.app",
|
||||
"versionCode": 38,
|
||||
"versionCode": 14,
|
||||
"architectures": [
|
||||
"arm64-v8a",
|
||||
"armeabi-v7a",
|
||||
|
|
@ -69,6 +57,7 @@
|
|||
],
|
||||
"jsEngine": "hermes"
|
||||
},
|
||||
|
||||
"extra": {
|
||||
"eas": {
|
||||
"projectId": "909107b8-fe61-45ce-b02f-b02510d306a6"
|
||||
|
|
@ -76,7 +65,6 @@
|
|||
},
|
||||
"owner": "nayifleo",
|
||||
"plugins": [
|
||||
"expo-live-activity",
|
||||
[
|
||||
"@sentry/react-native/expo",
|
||||
{
|
||||
|
|
@ -85,34 +73,20 @@
|
|||
"organization": "tapframe"
|
||||
}
|
||||
],
|
||||
[
|
||||
"@kesha-antonov/react-native-background-downloader",
|
||||
{
|
||||
"skipMmkvDependency": true
|
||||
}
|
||||
],
|
||||
"expo-localization",
|
||||
[
|
||||
"expo-updates",
|
||||
{
|
||||
"username": "nayifleo"
|
||||
}
|
||||
],
|
||||
"react-native-bottom-tabs",
|
||||
[
|
||||
"react-native-google-cast",
|
||||
{
|
||||
"receiverAppId": "CC1AD845",
|
||||
"iosStartDiscoveryAfterFirstTapOnCastButton": true
|
||||
}
|
||||
]
|
||||
],
|
||||
"updates": {
|
||||
"enabled": true,
|
||||
"checkAutomatically": "ON_ERROR_RECOVERY",
|
||||
"fallbackToCacheTimeout": 30000,
|
||||
"url": "https://ota.nuvioapp.space/api/manifest"
|
||||
"url": "https://grim-reyna-tapframe-69970143.koyeb.app/api/manifest"
|
||||
},
|
||||
"runtimeVersion": "1.4.2"
|
||||
"runtimeVersion": "1.1.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 14 KiB |
BIN
assets/WhatsApp Image 2025-04-22 at 18.46.30_ed1e0602.jpg
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
assets/WhatsApp Image 2025-04-24 at 09.47.46_1d72ceb0.jpg
Normal file
|
After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 52 KiB |
|
|
@ -1,4 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 6 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 7 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 64 KiB |
|
|
@ -1,3 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#000000</color>
|
||||
<color name="ic_launcher_background">#151515</color>
|
||||
</resources>
|
||||
|
Before Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 13 KiB |
BIN
assets/icon.png
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 1.1 MiB |
|
|
@ -1,128 +1,128 @@
|
|||
{
|
||||
"images": [
|
||||
"images":[
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"size": "20x20",
|
||||
"scale": "2x",
|
||||
"filename": "Icon-App-20x20@2x.png"
|
||||
"idiom":"iphone",
|
||||
"size":"20x20",
|
||||
"scale":"2x",
|
||||
"filename":"Icon-App-20x20@2x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"size": "20x20",
|
||||
"scale": "3x",
|
||||
"filename": "Icon-App-20x20@3x.png"
|
||||
"idiom":"iphone",
|
||||
"size":"20x20",
|
||||
"scale":"3x",
|
||||
"filename":"Icon-App-20x20@3x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"size": "29x29",
|
||||
"scale": "1x",
|
||||
"filename": "Icon-App-29x29@1x.png"
|
||||
"idiom":"iphone",
|
||||
"size":"29x29",
|
||||
"scale":"1x",
|
||||
"filename":"Icon-App-29x29@1x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"size": "29x29",
|
||||
"scale": "2x",
|
||||
"filename": "Icon-App-29x29@2x.png"
|
||||
"idiom":"iphone",
|
||||
"size":"29x29",
|
||||
"scale":"2x",
|
||||
"filename":"Icon-App-29x29@2x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"size": "29x29",
|
||||
"scale": "3x",
|
||||
"filename": "Icon-App-29x29@3x.png"
|
||||
"idiom":"iphone",
|
||||
"size":"29x29",
|
||||
"scale":"3x",
|
||||
"filename":"Icon-App-29x29@3x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"size": "40x40",
|
||||
"scale": "2x",
|
||||
"filename": "Icon-App-40x40@2x.png"
|
||||
"idiom":"iphone",
|
||||
"size":"40x40",
|
||||
"scale":"2x",
|
||||
"filename":"Icon-App-40x40@2x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"size": "40x40",
|
||||
"scale": "3x",
|
||||
"filename": "Icon-App-40x40@3x.png"
|
||||
"idiom":"iphone",
|
||||
"size":"40x40",
|
||||
"scale":"3x",
|
||||
"filename":"Icon-App-40x40@3x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"size": "60x60",
|
||||
"scale": "2x",
|
||||
"filename": "Icon-App-60x60@2x.png"
|
||||
"idiom":"iphone",
|
||||
"size":"60x60",
|
||||
"scale":"2x",
|
||||
"filename":"Icon-App-60x60@2x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"size": "60x60",
|
||||
"scale": "3x",
|
||||
"filename": "Icon-App-60x60@3x.png"
|
||||
"idiom":"iphone",
|
||||
"size":"60x60",
|
||||
"scale":"3x",
|
||||
"filename":"Icon-App-60x60@3x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "iphone",
|
||||
"size": "76x76",
|
||||
"scale": "2x",
|
||||
"filename": "Icon-App-76x76@2x.png"
|
||||
"idiom":"iphone",
|
||||
"size":"76x76",
|
||||
"scale":"2x",
|
||||
"filename":"Icon-App-76x76@2x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "ipad",
|
||||
"size": "20x20",
|
||||
"scale": "1x",
|
||||
"filename": "Icon-App-20x20@1x.png"
|
||||
"idiom":"ipad",
|
||||
"size":"20x20",
|
||||
"scale":"1x",
|
||||
"filename":"Icon-App-20x20@1x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "ipad",
|
||||
"size": "20x20",
|
||||
"scale": "2x",
|
||||
"filename": "Icon-App-20x20@2x.png"
|
||||
"idiom":"ipad",
|
||||
"size":"20x20",
|
||||
"scale":"2x",
|
||||
"filename":"Icon-App-20x20@2x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "ipad",
|
||||
"size": "29x29",
|
||||
"scale": "1x",
|
||||
"filename": "Icon-App-29x29@1x.png"
|
||||
"idiom":"ipad",
|
||||
"size":"29x29",
|
||||
"scale":"1x",
|
||||
"filename":"Icon-App-29x29@1x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "ipad",
|
||||
"size": "29x29",
|
||||
"scale": "2x",
|
||||
"filename": "Icon-App-29x29@2x.png"
|
||||
"idiom":"ipad",
|
||||
"size":"29x29",
|
||||
"scale":"2x",
|
||||
"filename":"Icon-App-29x29@2x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "ipad",
|
||||
"size": "40x40",
|
||||
"scale": "1x",
|
||||
"filename": "Icon-App-40x40@1x.png"
|
||||
"idiom":"ipad",
|
||||
"size":"40x40",
|
||||
"scale":"1x",
|
||||
"filename":"Icon-App-40x40@1x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "ipad",
|
||||
"size": "40x40",
|
||||
"scale": "2x",
|
||||
"filename": "Icon-App-40x40@2x.png"
|
||||
"idiom":"ipad",
|
||||
"size":"40x40",
|
||||
"scale":"2x",
|
||||
"filename":"Icon-App-40x40@2x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "ipad",
|
||||
"size": "76x76",
|
||||
"scale": "1x",
|
||||
"filename": "Icon-App-76x76@1x.png"
|
||||
"idiom":"ipad",
|
||||
"size":"76x76",
|
||||
"scale":"1x",
|
||||
"filename":"Icon-App-76x76@1x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "ipad",
|
||||
"size": "76x76",
|
||||
"scale": "2x",
|
||||
"filename": "Icon-App-76x76@2x.png"
|
||||
"idiom":"ipad",
|
||||
"size":"76x76",
|
||||
"scale":"2x",
|
||||
"filename":"Icon-App-76x76@2x.png"
|
||||
},
|
||||
{
|
||||
"idiom": "ipad",
|
||||
"size": "83.5x83.5",
|
||||
"scale": "2x",
|
||||
"filename": "Icon-App-83.5x83.5@2x.png"
|
||||
"idiom":"ipad",
|
||||
"size":"83.5x83.5",
|
||||
"scale":"2x",
|
||||
"filename":"Icon-App-83.5x83.5@2x.png"
|
||||
},
|
||||
{
|
||||
"size": "1024x1024",
|
||||
"idiom": "ios-marketing",
|
||||
"scale": "1x",
|
||||
"filename": "ItunesArtwork@2x.png"
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"filename" : "ItunesArtwork@2x.png"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "easyappicon"
|
||||
"info":{
|
||||
"version":1,
|
||||
"author":"easyappicon"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 718 B |
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.4 KiB |