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); }