mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
continue watching improvements. testflight link added to readme
This commit is contained in:
parent
8a34bf6678
commit
3c35b99759
6 changed files with 664 additions and 511 deletions
|
|
@ -66,6 +66,9 @@ Download the latest APK from [GitHub Releases](https://github.com/tapframe/Nuvio
|
|||
|
||||
### iOS
|
||||
|
||||
#### TestFlight (Recommended)
|
||||
<img src="https://upload.wikimedia.org/wikipedia/fr/b/bc/TestFlight-icon.png" width="24" height="24" align="left"> [](https://testflight.apple.com/join/QkKMGRqp)
|
||||
|
||||
#### AltStore
|
||||
<img src="https://upload.wikimedia.org/wikipedia/commons/2/20/AltStore_logo.png" width="24" height="24" align="left"> [](https://tinyurl.com/NuvioAltstore)
|
||||
|
||||
|
|
|
|||
818
index.html
818
index.html
File diff suppressed because it is too large
Load diff
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -1,103 +1,99 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Nuvio</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.2.10</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>nuvio</string>
|
||||
<string>com.nuvio.app</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>exp+nuvio</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>25</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>12.0</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_http._tcp</string>
|
||||
<string>_googlecast._tcp</string>
|
||||
<string>_CC1AD845._googlecast._tcp</string>
|
||||
</array>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>Allow $(PRODUCT_NAME) to access your local network</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>This app does not require microphone access.</string>
|
||||
<key>RCTNewArchEnabled</key>
|
||||
<true/>
|
||||
<key>RCTRootViewBackgroundColor</key>
|
||||
<integer>4278322180</integer>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>SplashScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>UIRequiresFullScreen</key>
|
||||
<true/>
|
||||
<key>UIStatusBarStyle</key>
|
||||
<string>UIStatusBarStyleDefault</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Dark</string>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Nuvio</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.2.10</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>nuvio</string>
|
||||
<string>com.nuvio.app</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>exp+nuvio</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>25</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>12.0</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_http._tcp</string>
|
||||
<string>_googlecast._tcp</string>
|
||||
<string>_CC1AD845._googlecast._tcp</string>
|
||||
</array>
|
||||
<key>RCTNewArchEnabled</key>
|
||||
<true/>
|
||||
<key>RCTRootViewBackgroundColor</key>
|
||||
<integer>4278322180</integer>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>SplashScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>UIRequiresFullScreen</key>
|
||||
<true/>
|
||||
<key>UIStatusBarStyle</key>
|
||||
<string>UIStatusBarStyleDefault</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Dark</string>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array/>
|
||||
</dict>
|
||||
</plist>
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -240,6 +240,44 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((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<ContinueWatchingRef>((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<ContinueWatchingRef>((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<ContinueWatchingRef>((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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue