Custom TTL for Stream cache
This commit is contained in:
parent
f0271cd395
commit
ce7f92b540
7 changed files with 321 additions and 146 deletions
|
|
@ -461,7 +461,7 @@
|
||||||
);
|
);
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app;
|
PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app;
|
||||||
PRODUCT_NAME = Nuvio;
|
PRODUCT_NAME = "Nuvio";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
|
|
@ -492,8 +492,8 @@
|
||||||
"-lc++",
|
"-lc++",
|
||||||
);
|
);
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.nuviohub.app;
|
PRODUCT_BUNDLE_IDENTIFIER = "com.nuvio.app";
|
||||||
PRODUCT_NAME = Nuvio;
|
PRODUCT_NAME = "Nuvio";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
|
|
||||||
|
|
@ -1,99 +1,101 @@
|
||||||
<?xml version="1.0" encoding="UTF-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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Nuvio</string>
|
<string>Nuvio</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>$(PRODUCT_NAME)</string>
|
<string>$(PRODUCT_NAME)</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.2.6</string>
|
<string>1.2.6</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>nuvio</string>
|
<string>nuvio</string>
|
||||||
<string>com.nuvio.app</string>
|
<string>com.nuvio.app</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>exp+nuvio</string>
|
<string>exp+nuvio</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>21</string>
|
<string>21</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>12.0</string>
|
<string>12.0</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
<key>NSBonjourServices</key>
|
<key>NSBonjourServices</key>
|
||||||
<array>
|
<array>
|
||||||
<string>_http._tcp</string>
|
<string>_http._tcp</string>
|
||||||
</array>
|
</array>
|
||||||
<key>NSLocalNetworkUsageDescription</key>
|
<key>NSLocalNetworkUsageDescription</key>
|
||||||
<string>Allow $(PRODUCT_NAME) to access your local network</string>
|
<string>Allow $(PRODUCT_NAME) to access your local network</string>
|
||||||
<key>RCTNewArchEnabled</key>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<true/>
|
<string>This app does not require microphone access.</string>
|
||||||
<key>RCTRootViewBackgroundColor</key>
|
<key>RCTNewArchEnabled</key>
|
||||||
<integer>4278322180</integer>
|
<true/>
|
||||||
<key>UIBackgroundModes</key>
|
<key>RCTRootViewBackgroundColor</key>
|
||||||
<array>
|
<integer>4278322180</integer>
|
||||||
<string>audio</string>
|
<key>UIBackgroundModes</key>
|
||||||
</array>
|
<array>
|
||||||
<key>UIFileSharingEnabled</key>
|
<string>audio</string>
|
||||||
<true/>
|
</array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UIFileSharingEnabled</key>
|
||||||
<string>SplashScreen</string>
|
<true/>
|
||||||
<key>UIRequiredDeviceCapabilities</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<array>
|
<string>SplashScreen</string>
|
||||||
<string>arm64</string>
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
</array>
|
<array>
|
||||||
<key>UIRequiresFullScreen</key>
|
<string>arm64</string>
|
||||||
<false/>
|
</array>
|
||||||
<key>UIStatusBarStyle</key>
|
<key>UIRequiresFullScreen</key>
|
||||||
<string>UIStatusBarStyleDefault</string>
|
<false/>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UIStatusBarStyle</key>
|
||||||
<array>
|
<string>UIStatusBarStyleDefault</string>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<array>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
</array>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
<array>
|
</array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<array>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
</array>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<key>UIUserInterfaceStyle</key>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
<string>Dark</string>
|
</array>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIUserInterfaceStyle</key>
|
||||||
<false/>
|
<string>Dark</string>
|
||||||
</dict>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
</plist>
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
<?xml version="1.0" encoding="UTF-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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict/>
|
<dict>
|
||||||
</plist>
|
<key>aps-environment</key>
|
||||||
|
<string>development</string>
|
||||||
|
<key>com.apple.developer.associated-domains</key>
|
||||||
|
<array/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -85,6 +85,7 @@ export interface AppSettings {
|
||||||
// Continue Watching behavior
|
// Continue Watching behavior
|
||||||
useCachedStreams: boolean; // Enable/disable direct player navigation from Continue Watching cache
|
useCachedStreams: boolean; // Enable/disable direct player navigation from Continue Watching cache
|
||||||
openMetadataScreenWhenCacheDisabled: boolean; // When cache disabled, open MetadataScreen instead of StreamsScreen
|
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 = {
|
export const DEFAULT_SETTINGS: AppSettings = {
|
||||||
|
|
@ -143,6 +144,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
||||||
// Continue Watching behavior
|
// Continue Watching behavior
|
||||||
useCachedStreams: false, // Enable by default
|
useCachedStreams: false, // Enable by default
|
||||||
openMetadataScreenWhenCacheDisabled: true, // Default to StreamsScreen when cache disabled
|
openMetadataScreenWhenCacheDisabled: true, // Default to StreamsScreen when cache disabled
|
||||||
|
streamCacheTTL: 60 * 60 * 1000, // Default: 1 hour in milliseconds
|
||||||
};
|
};
|
||||||
|
|
||||||
const SETTINGS_STORAGE_KEY = 'app_settings';
|
const SETTINGS_STORAGE_KEY = 'app_settings';
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,29 @@ import { useTheme } from '../contexts/ThemeContext';
|
||||||
import { useSettings } from '../hooks/useSettings';
|
import { useSettings } from '../hooks/useSettings';
|
||||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
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 ContinueWatchingSettingsScreen: React.FC = () => {
|
||||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||||
const { settings, updateSetting } = useSettings();
|
const { settings, updateSetting } = useSettings();
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
const colors = currentTheme.colors;
|
const colors = currentTheme.colors;
|
||||||
|
const styles = createStyles(colors);
|
||||||
const [showSavedIndicator, setShowSavedIndicator] = useState(false);
|
const [showSavedIndicator, setShowSavedIndicator] = useState(false);
|
||||||
const fadeAnim = React.useRef(new Animated.Value(0)).current;
|
const fadeAnim = React.useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
|
|
@ -96,7 +114,6 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
|
||||||
styles.settingItem,
|
styles.settingItem,
|
||||||
{
|
{
|
||||||
borderBottomColor: isLast ? 'transparent' : colors.border,
|
borderBottomColor: isLast ? 'transparent' : colors.border,
|
||||||
backgroundColor: colors.elevation1
|
|
||||||
}
|
}
|
||||||
]}>
|
]}>
|
||||||
<View style={styles.settingContent}>
|
<View style={styles.settingContent}>
|
||||||
|
|
@ -111,31 +128,52 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
const SectionHeader = ({ title }: { title: string }) => (
|
|
||||||
<View style={[styles.sectionHeader, { backgroundColor: colors.darkBackground }]}>
|
const TTLPickerItem = ({ option }: { option: { label: string; value: number } }) => {
|
||||||
<Text style={[styles.sectionTitle, { color: colors.highEmphasis }]}>
|
const isSelected = settings.streamCacheTTL === option.value;
|
||||||
{title}
|
return (
|
||||||
</Text>
|
<TouchableOpacity
|
||||||
</View>
|
style={[
|
||||||
);
|
styles.ttlOption,
|
||||||
|
{
|
||||||
|
backgroundColor: isSelected ? colors.primary : colors.elevation1,
|
||||||
|
borderColor: isSelected ? colors.primary : colors.border,
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
onPress={() => handleUpdateSetting('streamCacheTTL', option.value)}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Text style={[
|
||||||
|
styles.ttlOptionText,
|
||||||
|
{ color: isSelected ? colors.white : colors.highEmphasis }
|
||||||
|
]}>
|
||||||
|
{option.label}
|
||||||
|
</Text>
|
||||||
|
{isSelected && (
|
||||||
|
<MaterialIcons name="check" size={20} color={colors.white} />
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={[styles.container, { backgroundColor: colors.darkBackground }]}>
|
<SafeAreaView style={[styles.container, { backgroundColor: colors.darkBackground }]}>
|
||||||
<StatusBar barStyle="light-content" />
|
<StatusBar barStyle="light-content" />
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View style={[styles.header, { backgroundColor: colors.darkBackground }]}>
|
<View style={styles.header}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
onPress={handleBack}
|
onPress={handleBack}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="chevron-left" size={28} color={colors.primary} />
|
<MaterialIcons name="chevron-left" size={28} color={colors.white} />
|
||||||
<Text style={[styles.backText, { color: colors.primary }]}>Settings</Text>
|
<Text style={styles.backText}>Settings</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text style={[styles.headerTitle, { color: colors.highEmphasis }]}>
|
|
||||||
Continue Watching
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
<Text style={styles.headerTitle}>
|
||||||
|
Continue Watching
|
||||||
|
</Text>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<ScrollView
|
<ScrollView
|
||||||
|
|
@ -143,9 +181,9 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
contentContainerStyle={styles.contentContainer}
|
contentContainerStyle={styles.contentContainer}
|
||||||
>
|
>
|
||||||
<SectionHeader title="PLAYBACK BEHAVIOR" />
|
<View style={styles.section}>
|
||||||
|
<Text style={styles.sectionTitle}>PLAYBACK BEHAVIOR</Text>
|
||||||
<View style={[styles.settingsCard, { backgroundColor: colors.elevation1 }]}>
|
<View style={styles.settingsCard}>
|
||||||
<SettingItem
|
<SettingItem
|
||||||
title="Use Cached Streams"
|
title="Use Cached Streams"
|
||||||
description="When enabled, clicking Continue Watching items will open the player directly using previously played streams. When disabled, opens a content screen instead."
|
description="When enabled, clicking Continue Watching items will open the player directly using previously played streams. When disabled, opens a content screen instead."
|
||||||
|
|
@ -162,9 +200,52 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
|
||||||
isLast={true}
|
isLast={true}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={[styles.infoCard, { backgroundColor: colors.elevation1 }]}>
|
{settings.useCachedStreams && (
|
||||||
|
<View style={styles.section}>
|
||||||
|
<Text style={styles.sectionTitle}>CACHE SETTINGS</Text>
|
||||||
|
<View style={styles.settingsCard}>
|
||||||
|
<View style={[styles.settingItem, { borderBottomWidth: 0, flexDirection: 'column', alignItems: 'flex-start' }]}>
|
||||||
|
<Text style={[styles.settingTitle, { color: colors.highEmphasis, marginBottom: 8 }]}>
|
||||||
|
Stream Cache Duration
|
||||||
|
</Text>
|
||||||
|
<Text style={[styles.settingDescription, { color: colors.mediumEmphasis, marginBottom: 16 }]}>
|
||||||
|
How long to keep cached stream links before they expire
|
||||||
|
</Text>
|
||||||
|
<View style={styles.ttlOptionsContainer}>
|
||||||
|
{TTL_OPTIONS.map((row, rowIndex) => (
|
||||||
|
<View key={rowIndex} style={styles.ttlRow}>
|
||||||
|
{row.map((option) => (
|
||||||
|
<TTLPickerItem key={option.value} option={option} />
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{settings.useCachedStreams && (
|
||||||
|
<View style={styles.section}>
|
||||||
|
<View style={[styles.warningCard, { borderColor: colors.warning }]}>
|
||||||
|
<View style={styles.warningHeader}>
|
||||||
|
<MaterialIcons name="warning" size={20} color={colors.warning} />
|
||||||
|
<Text style={[styles.warningTitle, { color: colors.warning }]}>
|
||||||
|
Important Note
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={[styles.warningText, { color: colors.mediumEmphasis }]}>
|
||||||
|
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.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View style={styles.section}>
|
||||||
|
<View style={styles.infoCard}>
|
||||||
<View style={styles.infoHeader}>
|
<View style={styles.infoHeader}>
|
||||||
<MaterialIcons name="info" size={20} color={colors.primary} />
|
<MaterialIcons name="info" size={20} color={colors.primary} />
|
||||||
<Text style={[styles.infoTitle, { color: colors.highEmphasis }]}>
|
<Text style={[styles.infoTitle, { color: colors.highEmphasis }]}>
|
||||||
|
|
@ -172,12 +253,24 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={[styles.infoText, { color: colors.mediumEmphasis }]}>
|
<Text style={[styles.infoText, { color: colors.mediumEmphasis }]}>
|
||||||
• Streams are cached for 1 hour after playing{'\n'}
|
{settings.useCachedStreams ? (
|
||||||
• Cached streams are validated before use{'\n'}
|
<>
|
||||||
• If cache is invalid or expired, falls back to content screen{'\n'}
|
• Streams are cached for your selected duration after playing{'\n'}
|
||||||
• "Use Cached Streams" controls direct player vs screen navigation{'\n'}
|
• Cached streams are validated before use{'\n'}
|
||||||
• "Open Metadata Screen" appears only when cached streams are disabled
|
• 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
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
|
|
@ -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: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingVertical: 12,
|
paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight || 0) + 8 : 8,
|
||||||
paddingTop: Platform.OS === 'ios' ? 0 : 12,
|
|
||||||
},
|
},
|
||||||
backButton: {
|
backButton: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginRight: 16,
|
padding: 8,
|
||||||
},
|
},
|
||||||
backText: {
|
backText: {
|
||||||
fontSize: 16,
|
fontSize: 17,
|
||||||
fontWeight: '600',
|
fontWeight: '400',
|
||||||
marginLeft: 4,
|
color: colors.primary,
|
||||||
},
|
},
|
||||||
headerTitle: {
|
headerTitle: {
|
||||||
fontSize: 20,
|
fontSize: 34,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
flex: 1,
|
color: colors.white,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingBottom: 16,
|
||||||
|
paddingTop: 8,
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|
@ -230,26 +327,33 @@ const styles = StyleSheet.create({
|
||||||
contentContainer: {
|
contentContainer: {
|
||||||
paddingBottom: 100,
|
paddingBottom: 100,
|
||||||
},
|
},
|
||||||
sectionHeader: {
|
section: {
|
||||||
paddingHorizontal: 16,
|
marginBottom: 24,
|
||||||
paddingVertical: 12,
|
|
||||||
paddingTop: 24,
|
|
||||||
},
|
},
|
||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: '700',
|
fontWeight: '600',
|
||||||
|
color: colors.mediumGray,
|
||||||
|
marginHorizontal: 16,
|
||||||
|
marginBottom: 8,
|
||||||
letterSpacing: 0.5,
|
letterSpacing: 0.5,
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
},
|
},
|
||||||
settingsCard: {
|
settingsCard: {
|
||||||
marginHorizontal: 16,
|
marginHorizontal: 16,
|
||||||
|
backgroundColor: colors.elevation2,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
|
padding: 16,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 2,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
settingItem: {
|
settingItem: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingHorizontal: 16,
|
|
||||||
paddingVertical: 16,
|
paddingVertical: 16,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
},
|
},
|
||||||
|
|
@ -269,8 +373,14 @@ const styles = StyleSheet.create({
|
||||||
infoCard: {
|
infoCard: {
|
||||||
marginHorizontal: 16,
|
marginHorizontal: 16,
|
||||||
marginTop: 16,
|
marginTop: 16,
|
||||||
padding: 16,
|
backgroundColor: colors.elevation2,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
|
padding: 16,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 2,
|
||||||
},
|
},
|
||||||
infoHeader: {
|
infoHeader: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|
@ -304,6 +414,58 @@ const styles = StyleSheet.create({
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
marginLeft: 8,
|
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;
|
export default ContinueWatchingSettingsScreen;
|
||||||
|
|
|
||||||
|
|
@ -1159,7 +1159,8 @@ export const StreamsScreen = () => {
|
||||||
season,
|
season,
|
||||||
episode,
|
episode,
|
||||||
episodeTitle,
|
episodeTitle,
|
||||||
imdbId || undefined
|
imdbId || undefined,
|
||||||
|
settings.streamCacheTTL
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn('[StreamsScreen] Failed to save stream to cache:', error);
|
logger.warn('[StreamsScreen] Failed to save stream to cache:', error);
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ export interface StreamCacheEntry {
|
||||||
expiresAt: number; // Timestamp when cache expires
|
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_';
|
const CACHE_KEY_PREFIX = 'stream_cache_';
|
||||||
|
|
||||||
class StreamCacheService {
|
class StreamCacheService {
|
||||||
|
|
@ -34,7 +34,8 @@ class StreamCacheService {
|
||||||
season?: number,
|
season?: number,
|
||||||
episode?: number,
|
episode?: number,
|
||||||
episodeTitle?: string,
|
episodeTitle?: string,
|
||||||
imdbId?: string
|
imdbId?: string,
|
||||||
|
cacheDuration?: number
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const cacheKey = this.getCacheKey(id, type, episodeId);
|
const cacheKey = this.getCacheKey(id, type, episodeId);
|
||||||
|
|
@ -52,16 +53,18 @@ class StreamCacheService {
|
||||||
url: stream.url
|
url: stream.url
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ttl = cacheDuration || DEFAULT_CACHE_DURATION;
|
||||||
const cacheEntry: StreamCacheEntry = {
|
const cacheEntry: StreamCacheEntry = {
|
||||||
cachedStream,
|
cachedStream,
|
||||||
expiresAt: now + CACHE_DURATION
|
expiresAt: now + ttl
|
||||||
};
|
};
|
||||||
|
|
||||||
await AsyncStorage.setItem(cacheKey, JSON.stringify(cacheEntry));
|
await AsyncStorage.setItem(cacheKey, JSON.stringify(cacheEntry));
|
||||||
logger.log(`💾 [StreamCache] Saved stream cache for ${type}:${id}${episodeId ? `:${episodeId}` : ''}`);
|
logger.log(`💾 [StreamCache] Saved stream cache for ${type}:${id}${episodeId ? `:${episodeId}` : ''}`);
|
||||||
logger.log(`💾 [StreamCache] Cache key: ${cacheKey}`);
|
logger.log(`💾 [StreamCache] Cache key: ${cacheKey}`);
|
||||||
logger.log(`💾 [StreamCache] Stream URL: ${stream.url}`);
|
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) {
|
} catch (error) {
|
||||||
logger.warn('[StreamCache] Failed to save stream to cache:', error);
|
logger.warn('[StreamCache] Failed to save stream to cache:', error);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue