diff --git a/ios/Nuvio.xcodeproj/project.pbxproj b/ios/Nuvio.xcodeproj/project.pbxproj
index 63f7578..4bf304a 100644
--- a/ios/Nuvio.xcodeproj/project.pbxproj
+++ b/ios/Nuvio.xcodeproj/project.pbxproj
@@ -461,7 +461,7 @@
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app;
- PRODUCT_NAME = Nuvio;
+ PRODUCT_NAME = "Nuvio";
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@@ -492,8 +492,8 @@
"-lc++",
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
- PRODUCT_BUNDLE_IDENTIFIER = com.nuviohub.app;
- PRODUCT_NAME = Nuvio;
+ PRODUCT_BUNDLE_IDENTIFIER = "com.nuvio.app";
+ PRODUCT_NAME = "Nuvio";
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
diff --git a/ios/Nuvio/Info.plist b/ios/Nuvio/Info.plist
index 4ba2fc1..c4ebe4b 100644
--- a/ios/Nuvio/Info.plist
+++ b/ios/Nuvio/Info.plist
@@ -1,99 +1,101 @@
-
- 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.6
- CFBundleSignature
- ????
- CFBundleURLTypes
-
-
- CFBundleURLSchemes
-
- nuvio
- com.nuvio.app
-
-
-
- CFBundleURLSchemes
-
- exp+nuvio
-
-
-
- CFBundleVersion
- 21
- LSMinimumSystemVersion
- 12.0
- LSRequiresIPhoneOS
-
- LSSupportsOpeningDocumentsInPlace
-
- NSAppTransportSecurity
-
- NSAllowsArbitraryLoads
-
-
- NSBonjourServices
-
- _http._tcp
-
- NSLocalNetworkUsageDescription
- Allow $(PRODUCT_NAME) to access your local network
- 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
-
-
-
+
+ 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.6
+ CFBundleSignature
+ ????
+ CFBundleURLTypes
+
+
+ CFBundleURLSchemes
+
+ nuvio
+ com.nuvio.app
+
+
+
+ CFBundleURLSchemes
+
+ exp+nuvio
+
+
+
+ CFBundleVersion
+ 21
+ LSMinimumSystemVersion
+ 12.0
+ LSRequiresIPhoneOS
+
+ LSSupportsOpeningDocumentsInPlace
+
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoads
+
+
+ NSBonjourServices
+
+ _http._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
diff --git a/ios/Nuvio/NuvioRelease.entitlements b/ios/Nuvio/NuvioRelease.entitlements
index 0c67376..a0bc443 100644
--- a/ios/Nuvio/NuvioRelease.entitlements
+++ b/ios/Nuvio/NuvioRelease.entitlements
@@ -1,5 +1,10 @@
-
-
+
+ aps-environment
+ development
+ com.apple.developer.associated-domains
+
+
+
\ No newline at end of file
diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts
index 397150e..b01cf31 100644
--- a/src/hooks/useSettings.ts
+++ b/src/hooks/useSettings.ts
@@ -85,6 +85,7 @@ export interface AppSettings {
// Continue Watching behavior
useCachedStreams: boolean; // Enable/disable direct player navigation from Continue Watching cache
openMetadataScreenWhenCacheDisabled: boolean; // When cache disabled, open MetadataScreen instead of StreamsScreen
+ streamCacheTTL: number; // Stream cache duration in milliseconds (default: 1 hour)
}
export const DEFAULT_SETTINGS: AppSettings = {
@@ -143,6 +144,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
// Continue Watching behavior
useCachedStreams: false, // Enable by default
openMetadataScreenWhenCacheDisabled: true, // Default to StreamsScreen when cache disabled
+ streamCacheTTL: 60 * 60 * 1000, // Default: 1 hour in milliseconds
};
const SETTINGS_STORAGE_KEY = 'app_settings';
diff --git a/src/screens/ContinueWatchingSettingsScreen.tsx b/src/screens/ContinueWatchingSettingsScreen.tsx
index cc6266f..72bfdca 100644
--- a/src/screens/ContinueWatchingSettingsScreen.tsx
+++ b/src/screens/ContinueWatchingSettingsScreen.tsx
@@ -18,11 +18,29 @@ import { useTheme } from '../contexts/ThemeContext';
import { useSettings } from '../hooks/useSettings';
import { RootStackParamList } from '../navigation/AppNavigator';
+// TTL options in milliseconds - organized in rows
+const TTL_OPTIONS = [
+ [
+ { label: '15 min', value: 15 * 60 * 1000 },
+ { label: '30 min', value: 30 * 60 * 1000 },
+ { label: '1 hour', value: 60 * 60 * 1000 },
+ ],
+ [
+ { label: '2 hours', value: 2 * 60 * 60 * 1000 },
+ { label: '6 hours', value: 6 * 60 * 60 * 1000 },
+ { label: '12 hours', value: 12 * 60 * 60 * 1000 },
+ ],
+ [
+ { label: '24 hours', value: 24 * 60 * 60 * 1000 },
+ ],
+];
+
const ContinueWatchingSettingsScreen: React.FC = () => {
const navigation = useNavigation>();
const { settings, updateSetting } = useSettings();
const { currentTheme } = useTheme();
const colors = currentTheme.colors;
+ const styles = createStyles(colors);
const [showSavedIndicator, setShowSavedIndicator] = useState(false);
const fadeAnim = React.useRef(new Animated.Value(0)).current;
@@ -96,7 +114,6 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
styles.settingItem,
{
borderBottomColor: isLast ? 'transparent' : colors.border,
- backgroundColor: colors.elevation1
}
]}>
@@ -111,31 +128,52 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
);
- const SectionHeader = ({ title }: { title: string }) => (
-
-
- {title}
-
-
- );
+
+ const TTLPickerItem = ({ option }: { option: { label: string; value: number } }) => {
+ const isSelected = settings.streamCacheTTL === option.value;
+ return (
+ handleUpdateSetting('streamCacheTTL', option.value)}
+ activeOpacity={0.7}
+ >
+
+ {option.label}
+
+ {isSelected && (
+
+ )}
+
+ );
+ };
return (
{/* Header */}
-
-
+
-
- Settings
+
+ Settings
-
- Continue Watching
-
+
+
+ Continue Watching
+
{/* Content */}
{
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.contentContainer}
>
-
-
-
+
+ PLAYBACK BEHAVIOR
+
{
isLast={true}
/>
)}
+
-
+ {settings.useCachedStreams && (
+
+ CACHE SETTINGS
+
+
+
+ Stream Cache Duration
+
+
+ How long to keep cached stream links before they expire
+
+
+ {TTL_OPTIONS.map((row, rowIndex) => (
+
+ {row.map((option) => (
+
+ ))}
+
+ ))}
+
+
+
+
+ )}
+
+ {settings.useCachedStreams && (
+
+
+
+
+
+ Important Note
+
+
+
+ Not all stream links may remain active for the full cache duration. Longer cache times may result in expired links. If a cached link fails, the app will fall back to fetching fresh streams.
+
+
+
+ )}
+
+
+
@@ -172,12 +253,24 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
- • Streams are cached for 1 hour after playing{'\n'}
- • Cached streams are validated before use{'\n'}
- • If cache is invalid or expired, falls back to content screen{'\n'}
- • "Use Cached Streams" controls direct player vs screen navigation{'\n'}
- • "Open Metadata Screen" appears only when cached streams are disabled
+ {settings.useCachedStreams ? (
+ <>
+ • Streams are cached for your selected duration after playing{'\n'}
+ • Cached streams are validated before use{'\n'}
+ • If cache is invalid or expired, falls back to content screen{'\n'}
+ • "Use Cached Streams" controls direct player vs screen navigation{'\n'}
+ • "Open Metadata Screen" appears only when cached streams are disabled
+ >
+ ) : (
+ <>
+ • When cached streams are disabled, clicking Continue Watching items opens content screens{'\n'}
+ • "Open Metadata Screen" option controls which screen to open{'\n'}
+ • Metadata screen shows content details and allows manual stream selection{'\n'}
+ • Streams screen shows available streams for immediate playback
+ >
+ )}
+
@@ -198,31 +291,35 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
);
};
-const styles = StyleSheet.create({
+// Create a styles creator function that accepts the theme colors
+const createStyles = (colors: any) => StyleSheet.create({
container: {
flex: 1,
},
header: {
flexDirection: 'row',
alignItems: 'center',
+ justifyContent: 'space-between',
paddingHorizontal: 16,
- paddingVertical: 12,
- paddingTop: Platform.OS === 'ios' ? 0 : 12,
+ paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight || 0) + 8 : 8,
},
backButton: {
flexDirection: 'row',
alignItems: 'center',
- marginRight: 16,
+ padding: 8,
},
backText: {
- fontSize: 16,
- fontWeight: '600',
- marginLeft: 4,
+ fontSize: 17,
+ fontWeight: '400',
+ color: colors.primary,
},
headerTitle: {
- fontSize: 20,
+ fontSize: 34,
fontWeight: '700',
- flex: 1,
+ color: colors.white,
+ paddingHorizontal: 16,
+ paddingBottom: 16,
+ paddingTop: 8,
},
content: {
flex: 1,
@@ -230,26 +327,33 @@ const styles = StyleSheet.create({
contentContainer: {
paddingBottom: 100,
},
- sectionHeader: {
- paddingHorizontal: 16,
- paddingVertical: 12,
- paddingTop: 24,
+ section: {
+ marginBottom: 24,
},
sectionTitle: {
fontSize: 13,
- fontWeight: '700',
+ fontWeight: '600',
+ color: colors.mediumGray,
+ marginHorizontal: 16,
+ marginBottom: 8,
letterSpacing: 0.5,
textTransform: 'uppercase',
},
settingsCard: {
marginHorizontal: 16,
+ backgroundColor: colors.elevation2,
borderRadius: 12,
+ padding: 16,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 2,
overflow: 'hidden',
},
settingItem: {
flexDirection: 'row',
alignItems: 'center',
- paddingHorizontal: 16,
paddingVertical: 16,
borderBottomWidth: 1,
},
@@ -269,8 +373,14 @@ const styles = StyleSheet.create({
infoCard: {
marginHorizontal: 16,
marginTop: 16,
- padding: 16,
+ backgroundColor: colors.elevation2,
borderRadius: 12,
+ padding: 16,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 2,
},
infoHeader: {
flexDirection: 'row',
@@ -304,6 +414,58 @@ const styles = StyleSheet.create({
fontWeight: '600',
marginLeft: 8,
},
+ ttlOptionsContainer: {
+ width: '100%',
+ gap: 8,
+ },
+ ttlRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ width: '100%',
+ gap: 8,
+ },
+ ttlOption: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 12,
+ paddingVertical: 10,
+ borderRadius: 8,
+ borderWidth: 1,
+ flex: 1,
+ justifyContent: 'center',
+ gap: 6,
+ },
+ ttlOptionText: {
+ fontSize: 14,
+ fontWeight: '600',
+ },
+ warningCard: {
+ marginHorizontal: 16,
+ marginTop: 16,
+ backgroundColor: colors.elevation2,
+ borderRadius: 12,
+ padding: 16,
+ borderWidth: 1,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 2,
+ },
+ warningHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 12,
+ },
+ warningTitle: {
+ fontSize: 16,
+ fontWeight: '600',
+ marginLeft: 8,
+ },
+ warningText: {
+ fontSize: 14,
+ lineHeight: 20,
+ },
});
export default ContinueWatchingSettingsScreen;
diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx
index e525753..10ebf27 100644
--- a/src/screens/StreamsScreen.tsx
+++ b/src/screens/StreamsScreen.tsx
@@ -1159,7 +1159,8 @@ export const StreamsScreen = () => {
season,
episode,
episodeTitle,
- imdbId || undefined
+ imdbId || undefined,
+ settings.streamCacheTTL
);
} catch (error) {
logger.warn('[StreamsScreen] Failed to save stream to cache:', error);
diff --git a/src/services/streamCacheService.ts b/src/services/streamCacheService.ts
index ee59edb..2b55fce 100644
--- a/src/services/streamCacheService.ts
+++ b/src/services/streamCacheService.ts
@@ -18,7 +18,7 @@ export interface StreamCacheEntry {
expiresAt: number; // Timestamp when cache expires
}
-const CACHE_DURATION = 60 * 60 * 1000; // 1 hour in milliseconds
+const DEFAULT_CACHE_DURATION = 60 * 60 * 1000; // 1 hour in milliseconds (fallback)
const CACHE_KEY_PREFIX = 'stream_cache_';
class StreamCacheService {
@@ -34,7 +34,8 @@ class StreamCacheService {
season?: number,
episode?: number,
episodeTitle?: string,
- imdbId?: string
+ imdbId?: string,
+ cacheDuration?: number
): Promise {
try {
const cacheKey = this.getCacheKey(id, type, episodeId);
@@ -52,16 +53,18 @@ class StreamCacheService {
url: stream.url
};
+ const ttl = cacheDuration || DEFAULT_CACHE_DURATION;
const cacheEntry: StreamCacheEntry = {
cachedStream,
- expiresAt: now + CACHE_DURATION
+ expiresAt: now + ttl
};
await AsyncStorage.setItem(cacheKey, JSON.stringify(cacheEntry));
logger.log(`💾 [StreamCache] Saved stream cache for ${type}:${id}${episodeId ? `:${episodeId}` : ''}`);
logger.log(`💾 [StreamCache] Cache key: ${cacheKey}`);
logger.log(`💾 [StreamCache] Stream URL: ${stream.url}`);
- logger.log(`💾 [StreamCache] Expires at: ${new Date(now + CACHE_DURATION).toISOString()}`);
+ logger.log(`💾 [StreamCache] TTL: ${ttl / 1000 / 60} minutes`);
+ logger.log(`💾 [StreamCache] Expires at: ${new Date(now + ttl).toISOString()}`);
} catch (error) {
logger.warn('[StreamCache] Failed to save stream to cache:', error);
}