From 3c35b9975934dee4c5cf7746ef23fcff6c2db78d Mon Sep 17 00:00:00 2001 From: tapframe Date: Sat, 13 Dec 2025 21:09:09 +0530 Subject: [PATCH] continue watching improvements. testflight link added to readme --- README.md | 3 + index.html | 818 ++++++++++-------- ios/Nuvio.xcodeproj/project.pbxproj | 8 +- ios/Nuvio/Info.plist | 196 ++--- ios/Nuvio/NuvioRelease.entitlements | 12 +- .../home/ContinueWatchingSection.tsx | 138 +-- 6 files changed, 664 insertions(+), 511 deletions(-) diff --git a/README.md b/README.md index b595cedf..9b2e0570 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,9 @@ Download the latest APK from [GitHub Releases](https://github.com/tapframe/Nuvio ### iOS +#### TestFlight (Recommended) + [![Join TestFlight](https://img.shields.io/badge/Join-TestFlight-blue?style=for-the-badge)](https://testflight.apple.com/join/QkKMGRqp) + #### AltStore [![Add to AltStore](https://img.shields.io/badge/Add%20to-AltStore-blue?style=for-the-badge)](https://tinyurl.com/NuvioAltstore) diff --git a/index.html b/index.html index c11fbe2d..e617241b 100644 --- a/index.html +++ b/index.html @@ -1,10 +1,12 @@ + Nuvio - Media Hub - + +
@@ -1554,20 +1626,21 @@

NUVIO

The Ultimate Open-Source Media Experience

- + - +
-
- Direct Download -
-
-
Direct Download
-
Download the IPA file directly to your device
-
-
- - -
- AltStore -
-
-
Install via AltStore
-
One-click installation through AltStore
-
-
- - -
- SideStore -
-
-
Install via SideStore
-
One-click installation through SideStore
-
-
- + +
+ TestFlight +
+
+
TestFlight (Recommended)
+
Install via Apple's official beta testing platform
+
+
+ + +
+ Direct Download +
+
+
Direct Download
+
Download the IPA file directly to your device
+
+
+ + +
+ AltStore +
+
+
Install via AltStore
+
One-click installation through AltStore
+
+
+ + +
+ SideStore +
+
+
Install via SideStore
+
One-click installation through SideStore
+
+
+
- Copy URL + Copy URL
Copy Source URL
@@ -1639,56 +1725,70 @@
- - - + + +

Stremio Addon Support

-

Full compatibility with Stremio addons, allowing you to access your favorite content providers seamlessly.

+

Full compatibility with Stremio addons, allowing you to access your favorite content providers + seamlessly.

- +
- +

Advanced Rating System

-

Comprehensive rating screens with IMDB, TMDB, Rotten Tomatoes, and Metacritic scores for informed viewing decisions.

+

Comprehensive rating screens with IMDB, TMDB, Rotten Tomatoes, and Metacritic scores for informed + viewing decisions.

- - + +

Deep Customization

-

Extensive customization options including themes, player settings, notification preferences, and personalized content discovery.

+

Extensive customization options including themes, player settings, notification preferences, and + personalized content discovery.

- - + +

Watch Progress Tracking

-

Seamless progress synchronization across devices with Trakt.tv integration and local watch history management.

+

Seamless progress synchronization across devices with Trakt.tv integration and local watch + history management.

- - + +

Multi-Platform Support

-

Available on iOS and Android platforms with consistent experience and cross-device synchronization

+

Available on iOS and Android platforms with consistent experience and cross-device + synchronization

@@ -1698,44 +1798,50 @@

SEE IT IN ACTION

-
- Home Screen -

Home Screen

-
-
- App Interface -

Details Page

-
-
- Home Screen 2 -

Home Screen 2

-
-
- Library -

Library

-
-
- Player Loading -

Player Loading

-
-
- Video Player -

Video Player

-
-
- Ratings -

Ratings

-
-
- Episodes & Seasons -

Episodes & Seasons

-
-
- Search & Details -

Search & Details

-
+
+ Home Screen +

Home Screen

+
+
+ App Interface +

Details Page

+
+
+ Home Screen 2 +

Home Screen 2

+
+
+ Library +

Library

+
+
+ Player Loading +

Player Loading

+
+
+ Video Player +

Video Player

+
+
+ Ratings +

Ratings

+
+
+ Episodes & Seasons +

Episodes & Seasons

+
+
+ Search & Details +

Search & Details

+
-
+
@@ -1745,16 +1851,18 @@

Privacy Policy

Last updated: January 2025

- +

No Account Sync

-

Nuvio operates entirely offline regarding user data. We do not have servers to store your account, preferences, or viewing history. All data is stored locally on your device.

+

Nuvio operates entirely offline regarding user data. We do not have servers to + store your account, preferences, or viewing history. All data is stored locally on your device. +

@@ -1765,32 +1873,43 @@
  • Watch history and progress
  • App settings and customization
  • -

    Important: Since data is stored only on your device, you are responsible for backing it up. If you uninstall the app or clear its data without a backup, your personalized data will be lost permanently.

    +

    Important: Since data is stored only on your device, you are responsible for + backing it up. If you uninstall the app or clear its data without a backup, your personalized + data will be lost permanently.

    Third-Party Services

    Nuvio integrates with external services to provide content and features:

      -
    • TMDB (The Movie Database): Used to fetch metadata like posters, plot summaries, and cast info.
    • -
    • Trakt.tv (Optional): If you choose to connect your account, your watch history will be synced with Trakt.tv subject to their privacy policy.
    • -
    • Sentry: We use Sentry for anonymous crash reporting to help us identify and fix bugs. No personal identifiable information (PII) is sent.
    • +
    • TMDB (The Movie Database): Used to fetch metadata like posters, plot + summaries, and cast info.
    • +
    • Trakt.tv (Optional): If you choose to connect your account, your watch + history will be synced with Trakt.tv subject to their privacy policy.
    • +
    • Sentry: We use Sentry for anonymous crash reporting to help us identify and + fix bugs. No personal identifiable information (PII) is sent.

    Content Disclaimer

    -

    Nuvio is a media player and aggregator. We do not host any content. All video content is provided by user-installed addons. Nuvio has no control over and assumes no responsibility for the content provided by third-party addons.

    +

    Nuvio is a media player and aggregator. We do not host any content. All video + content is provided by user-installed addons. Nuvio has no control over and assumes no + responsibility for the content provided by third-party addons.

    Open Source

    -

    Nuvio is open-source software. You can review our source code to verify our data handling practices on our GitHub repository.

    +

    Nuvio is open-source software. You can review our source code to verify our data handling + practices on our GitHub + repository.

    Contact

    -

    Questions or concerns? Please reach out via our GitHub Issues.

    +

    Questions or concerns? Please reach out via our GitHub Issues. +

    @@ -1801,31 +1920,37 @@

    Special Thanks

    -
    - -
    -
    -
    - - -
    -
    -
    - -
    -
    - -
    -
    +
    + +
    +
    +
    + + +
    +
    +
    + +
    +
    + +
    +

    Built with ❤️ using React Native & Expo

    - - + + - - + - + - + - + + \ No newline at end of file diff --git a/ios/Nuvio.xcodeproj/project.pbxproj b/ios/Nuvio.xcodeproj/project.pbxproj index 38083639..ce6a175d 100644 --- a/ios/Nuvio.xcodeproj/project.pbxproj +++ b/ios/Nuvio.xcodeproj/project.pbxproj @@ -477,7 +477,7 @@ ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app; - PRODUCT_NAME = "Nuvio"; + PRODUCT_NAME = Nuvio; SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -494,7 +494,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Nuvio/NuvioRelease.entitlements; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = NLXTHANK2N; + DEVELOPMENT_TEAM = 8QBDZ766S3; INFOPLIST_FILE = Nuvio/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.1; LD_RUNPATH_SEARCH_PATHS = ( @@ -508,8 +508,8 @@ "-lc++", ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; - PRODUCT_BUNDLE_IDENTIFIER = "com.nuvio.app"; - PRODUCT_NAME = "Nuvio"; + PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.hub; + PRODUCT_NAME = Nuvio; SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/ios/Nuvio/Info.plist b/ios/Nuvio/Info.plist index 14731e58..40a35d5d 100644 --- a/ios/Nuvio/Info.plist +++ b/ios/Nuvio/Info.plist @@ -1,103 +1,99 @@ - - CADisableMinimumFrameDurationOnPhone - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Nuvio - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.2.10 - CFBundleSignature - ???? - CFBundleURLTypes - - - CFBundleURLSchemes - - nuvio - com.nuvio.app - - - - CFBundleURLSchemes - - exp+nuvio - - - - CFBundleVersion - 25 - LSMinimumSystemVersion - 12.0 - LSRequiresIPhoneOS - - LSSupportsOpeningDocumentsInPlace - - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - - NSBonjourServices - - _http._tcp - _googlecast._tcp - _CC1AD845._googlecast._tcp - - NSLocalNetworkUsageDescription - Allow $(PRODUCT_NAME) to access your local network - NSMicrophoneUsageDescription - This app does not require microphone access. - RCTNewArchEnabled - - RCTRootViewBackgroundColor - 4278322180 - UIBackgroundModes - - audio - - UIFileSharingEnabled - - UILaunchStoryboardName - SplashScreen - UIRequiredDeviceCapabilities - - arm64 - - UIRequiresFullScreen - - UIStatusBarStyle - UIStatusBarStyleDefault - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIUserInterfaceStyle - Dark - UIViewControllerBasedStatusBarAppearance - - - \ No newline at end of file + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Nuvio + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.2.10 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleURLSchemes + + nuvio + com.nuvio.app + + + + CFBundleURLSchemes + + exp+nuvio + + + + CFBundleVersion + 25 + LSMinimumSystemVersion + 12.0 + LSRequiresIPhoneOS + + LSSupportsOpeningDocumentsInPlace + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSBonjourServices + + _http._tcp + _googlecast._tcp + _CC1AD845._googlecast._tcp + + RCTNewArchEnabled + + RCTRootViewBackgroundColor + 4278322180 + UIBackgroundModes + + audio + + UIFileSharingEnabled + + UILaunchStoryboardName + SplashScreen + UIRequiredDeviceCapabilities + + arm64 + + UIRequiresFullScreen + + UIStatusBarStyle + UIStatusBarStyleDefault + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIUserInterfaceStyle + Dark + UIViewControllerBasedStatusBarAppearance + + + diff --git a/ios/Nuvio/NuvioRelease.entitlements b/ios/Nuvio/NuvioRelease.entitlements index a0bc443f..903def2a 100644 --- a/ios/Nuvio/NuvioRelease.entitlements +++ b/ios/Nuvio/NuvioRelease.entitlements @@ -1,10 +1,8 @@ - - aps-environment - development - com.apple.developer.associated-domains - - - \ No newline at end of file + + aps-environment + development + + diff --git a/src/components/home/ContinueWatchingSection.tsx b/src/components/home/ContinueWatchingSection.tsx index 9d062c29..40b42516 100644 --- a/src/components/home/ContinueWatchingSection.tsx +++ b/src/components/home/ContinueWatchingSection.tsx @@ -240,6 +240,44 @@ const ContinueWatchingSection = React.forwardRef((props, re } }, []); + // Helper function to find the next episode + const findNextEpisode = useCallback((currentSeason: number, currentEpisode: number, videos: any[]) => { + if (!videos || !Array.isArray(videos)) return null; + + // Sort videos to ensure correct order + const sortedVideos = [...videos].sort((a, b) => { + if (a.season !== b.season) return a.season - b.season; + return a.episode - b.episode; + }); + + // Strategy 1: Look for next episode in the same season + let nextEp = sortedVideos.find(v => v.season === currentSeason && v.episode === currentEpisode + 1); + + // Strategy 2: If not found, look for the first episode of the next season + if (!nextEp) { + nextEp = sortedVideos.find(v => v.season === currentSeason + 1 && v.episode === 1); + } + + // Strategy 3: Just find the very next video in the list after the current one + // This handles cases where episode numbering isn't sequential or S+1 E1 isn't the standard start + if (!nextEp) { + const currentIndex = sortedVideos.findIndex(v => v.season === currentSeason && v.episode === currentEpisode); + if (currentIndex !== -1 && currentIndex + 1 < sortedVideos.length) { + const candidate = sortedVideos[currentIndex + 1]; + // Ensure we didn't just jump to a random special; check reasonable bounds if needed, + // but generally taking the next sorted item is correct for sequential viewing. + nextEp = candidate; + } + } + + // Verify the found episode is released + if (nextEp && isEpisodeReleased(nextEp)) { + return nextEp; + } + + return null; + }, []); + // Modified loadContinueWatching to render incrementally const loadContinueWatching = useCallback(async (isBackgroundRefresh = false) => { if (isRefreshingRef.current) { @@ -432,42 +470,42 @@ const ContinueWatchingSection = React.forwardRef((props, re const { episodeId, progress, progressPercent } = episode; if (group.type === 'series' && progressPercent >= 85) { - let nextSeason: number | undefined; - let nextEpisode: number | undefined; + // Local progress completion check if (episodeId) { + let currentSeason: number | undefined; + let currentEpisode: number | undefined; + const match = episodeId.match(/s(\d+)e(\d+)/i); if (match) { - const currentSeason = parseInt(match[1], 10); - const currentEpisode = parseInt(match[2], 10); - nextSeason = currentSeason; - nextEpisode = currentEpisode + 1; + currentSeason = parseInt(match[1], 10); + currentEpisode = parseInt(match[2], 10); } else { const parts = episodeId.split(':'); if (parts.length >= 2) { const seasonNum = parseInt(parts[parts.length - 2], 10); const episodeNum = parseInt(parts[parts.length - 1], 10); if (!isNaN(seasonNum) && !isNaN(episodeNum)) { - nextSeason = seasonNum; - nextEpisode = episodeNum + 1; + currentSeason = seasonNum; + currentEpisode = episodeNum; } } } - } - if (nextSeason !== undefined && nextEpisode !== undefined && metadata?.videos && Array.isArray(metadata.videos)) { - const nextEpisodeVideo = metadata.videos.find((video: any) => - video.season === nextSeason && video.episode === nextEpisode - ); - if (nextEpisodeVideo && isEpisodeReleased(nextEpisodeVideo)) { - batch.push({ - ...basicContent, - id: group.id, - type: group.type, - progress: 0, - lastUpdated: progress.lastUpdated, - season: nextSeason, - episode: nextEpisode, - episodeTitle: `Episode ${nextEpisode}`, - } as ContinueWatchingItem); + + if (currentSeason !== undefined && currentEpisode !== undefined && metadata?.videos) { + const nextEpisodeVideo = findNextEpisode(currentSeason, currentEpisode, metadata.videos); + + if (nextEpisodeVideo) { + batch.push({ + ...basicContent, + id: group.id, + type: group.type, + progress: 0, + lastUpdated: progress.lastUpdated, + season: nextEpisodeVideo.season, + episode: nextEpisodeVideo.episode, + episodeTitle: `Episode ${nextEpisodeVideo.episode}`, + } as ContinueWatchingItem); + } } } continue; @@ -532,23 +570,18 @@ const ContinueWatchingSection = React.forwardRef((props, re // If watched on Trakt, treat it as completed (try to find next episode) if (isWatchedOnTrakt) { - let nextSeason = season; - let nextEpisode = (episodeNumber || 0) + 1; - - if (nextSeason !== undefined && nextEpisode !== undefined && metadata?.videos && Array.isArray(metadata.videos)) { - const nextEpisodeVideo = metadata.videos.find((video: any) => - video.season === nextSeason && video.episode === nextEpisode - ); - if (nextEpisodeVideo && isEpisodeReleased(nextEpisodeVideo)) { + if (season !== undefined && episodeNumber !== undefined && metadata?.videos) { + const nextEpisodeVideo = findNextEpisode(season, episodeNumber, metadata.videos); + if (nextEpisodeVideo) { batch.push({ ...basicContent, id: group.id, type: group.type, progress: 0, lastUpdated: progress.lastUpdated, - season: nextSeason, - episode: nextEpisode, - episodeTitle: `Episode ${nextEpisode}`, + season: nextEpisodeVideo.season, + episode: nextEpisodeVideo.episode, + episodeTitle: `Episode ${nextEpisodeVideo.episode}`, } as ContinueWatchingItem); } } @@ -614,28 +647,25 @@ const ContinueWatchingSection = React.forwardRef((props, re continue; } - const nextEpisode = info.episode + 1; const cachedData = await getCachedMetadata('series', showId); if (!cachedData?.basicContent) continue; const { metadata, basicContent } = cachedData; - let nextEpisodeVideo = null; - if (metadata?.videos && Array.isArray(metadata.videos)) { - nextEpisodeVideo = metadata.videos.find((video: any) => - video.season === info.season && video.episode === nextEpisode - ); - } - if (nextEpisodeVideo && isEpisodeReleased(nextEpisodeVideo)) { - logger.log(`➕ [TraktSync] Adding next episode for ${showId}: S${info.season}E${nextEpisode}`); - traktBatch.push({ - ...basicContent, - id: showId, - type: 'series', - progress: 0, - lastUpdated: info.watchedAt, - season: info.season, - episode: nextEpisode, - episodeTitle: `Episode ${nextEpisode}`, - } as ContinueWatchingItem); + + if (metadata?.videos) { + const nextEpisodeVideo = findNextEpisode(info.season, info.episode, metadata.videos); + if (nextEpisodeVideo) { + logger.log(`➕ [TraktSync] Adding next episode for ${showId}: S${nextEpisodeVideo.season}E${nextEpisodeVideo.episode}`); + traktBatch.push({ + ...basicContent, + id: showId, + type: 'series', + progress: 0, + lastUpdated: info.watchedAt, + season: nextEpisodeVideo.season, + episode: nextEpisodeVideo.episode, + episodeTitle: `Episode ${nextEpisodeVideo.episode}`, + } as ContinueWatchingItem); + } } // Persist "watched" progress for the episode that Trakt reported (only if not recently removed)