continue watching changes
|
|
@ -94,8 +94,8 @@ android {
|
||||||
applicationId 'com.nuvio.app'
|
applicationId 'com.nuvio.app'
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 20
|
versionCode 21
|
||||||
versionName "1.2.5"
|
versionName "1.2.6"
|
||||||
|
|
||||||
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
|
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
|
||||||
}
|
}
|
||||||
|
|
@ -117,7 +117,7 @@ android {
|
||||||
def abiVersionCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4]
|
def abiVersionCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4]
|
||||||
applicationVariants.all { variant ->
|
applicationVariants.all { variant ->
|
||||||
variant.outputs.each { output ->
|
variant.outputs.each { output ->
|
||||||
def baseVersionCode = 20 // Current versionCode from defaultConfig
|
def baseVersionCode = 21 // Current versionCode 21 from defaultConfig
|
||||||
def abiName = output.getFilter(com.android.build.OutputFile.ABI)
|
def abiName = output.getFilter(com.android.build.OutputFile.ABI)
|
||||||
|
|
||||||
def versionCode = baseVersionCode * 100 // Base multiplier
|
def versionCode = baseVersionCode * 100 // Base multiplier
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 173 KiB |
|
|
@ -3,5 +3,5 @@
|
||||||
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
|
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
|
||||||
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
|
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
|
||||||
<string name="expo_system_ui_user_interface_style" translatable="false">dark</string>
|
<string name="expo_system_ui_user_interface_style" translatable="false">dark</string>
|
||||||
<string name="expo_runtime_version">1.2.5</string>
|
<string name="expo_runtime_version">1.2.6</string>
|
||||||
</resources>
|
</resources>
|
||||||
8
app.json
|
|
@ -2,7 +2,7 @@
|
||||||
"expo": {
|
"expo": {
|
||||||
"name": "Nuvio",
|
"name": "Nuvio",
|
||||||
"slug": "nuvio",
|
"slug": "nuvio",
|
||||||
"version": "1.2.5",
|
"version": "1.2.6",
|
||||||
"orientation": "default",
|
"orientation": "default",
|
||||||
"backgroundColor": "#020404",
|
"backgroundColor": "#020404",
|
||||||
"icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png",
|
"icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png",
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": true,
|
"supportsTablet": true,
|
||||||
"icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png",
|
"icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png",
|
||||||
"buildNumber": "20",
|
"buildNumber": "21",
|
||||||
"infoPlist": {
|
"infoPlist": {
|
||||||
"NSAppTransportSecurity": {
|
"NSAppTransportSecurity": {
|
||||||
"NSAllowsArbitraryLoads": true
|
"NSAllowsArbitraryLoads": true
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
"WAKE_LOCK"
|
"WAKE_LOCK"
|
||||||
],
|
],
|
||||||
"package": "com.nuvio.app",
|
"package": "com.nuvio.app",
|
||||||
"versionCode": 20,
|
"versionCode": 21,
|
||||||
"architectures": [
|
"architectures": [
|
||||||
"arm64-v8a",
|
"arm64-v8a",
|
||||||
"armeabi-v7a",
|
"armeabi-v7a",
|
||||||
|
|
@ -95,6 +95,6 @@
|
||||||
"fallbackToCacheTimeout": 30000,
|
"fallbackToCacheTimeout": 30000,
|
||||||
"url": "https://grim-reyna-tapframe-69970143.koyeb.app/api/manifest"
|
"url": "https://grim-reyna-tapframe-69970143.koyeb.app/api/manifest"
|
||||||
},
|
},
|
||||||
"runtimeVersion": "1.2.5"
|
"runtimeVersion": "1.2.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -460,7 +460,7 @@
|
||||||
"-lc++",
|
"-lc++",
|
||||||
);
|
);
|
||||||
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";
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 263 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 263 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 263 KiB After Width: | Height: | Size: 1.2 MiB |
|
|
@ -19,7 +19,7 @@
|
||||||
<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.5</string>
|
<string>1.2.6</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
|
|
@ -39,7 +39,7 @@
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>20</string>
|
<string>21</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>12.0</string>
|
<string>12.0</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
<key>EXUpdatesLaunchWaitMs</key>
|
<key>EXUpdatesLaunchWaitMs</key>
|
||||||
<integer>30000</integer>
|
<integer>30000</integer>
|
||||||
<key>EXUpdatesRuntimeVersion</key>
|
<key>EXUpdatesRuntimeVersion</key>
|
||||||
<string>1.2.5</string>
|
<string>1.2.6</string>
|
||||||
<key>EXUpdatesURL</key>
|
<key>EXUpdatesURL</key>
|
||||||
<string>https://grim-reyna-tapframe-69970143.koyeb.app/api/manifest</string>
|
<string>https://grim-reyna-tapframe-69970143.koyeb.app/api/manifest</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import * as Haptics from 'expo-haptics';
|
||||||
import { TraktService } from '../../services/traktService';
|
import { TraktService } from '../../services/traktService';
|
||||||
import { stremioService } from '../../services/stremioService';
|
import { stremioService } from '../../services/stremioService';
|
||||||
import { streamCacheService } from '../../services/streamCacheService';
|
import { streamCacheService } from '../../services/streamCacheService';
|
||||||
|
import { useSettings } from '../../hooks/useSettings';
|
||||||
import CustomAlert from '../../components/CustomAlert';
|
import CustomAlert from '../../components/CustomAlert';
|
||||||
|
|
||||||
// Define interface for continue watching items
|
// Define interface for continue watching items
|
||||||
|
|
@ -99,6 +100,7 @@ const isEpisodeReleased = (video: any): boolean => {
|
||||||
const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, ref) => {
|
const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, ref) => {
|
||||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
|
const { settings } = useSettings();
|
||||||
const [continueWatchingItems, setContinueWatchingItems] = useState<ContinueWatchingItem[]>([]);
|
const [continueWatchingItems, setContinueWatchingItems] = useState<ContinueWatchingItem[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const appState = useRef(AppState.currentState);
|
const appState = useRef(AppState.currentState);
|
||||||
|
|
@ -656,6 +658,45 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
try {
|
try {
|
||||||
logger.log(`🎬 [ContinueWatching] User clicked on: ${item.name} (${item.type}:${item.id})`);
|
logger.log(`🎬 [ContinueWatching] User clicked on: ${item.name} (${item.type}:${item.id})`);
|
||||||
|
|
||||||
|
// Check if cached streams are enabled in settings
|
||||||
|
if (!settings.useCachedStreams) {
|
||||||
|
logger.log(`📺 [ContinueWatching] Cached streams disabled, navigating to ${settings.openMetadataScreenWhenCacheDisabled ? 'MetadataScreen' : 'StreamsScreen'} for ${item.name}`);
|
||||||
|
|
||||||
|
// Navigate based on the second setting
|
||||||
|
if (settings.openMetadataScreenWhenCacheDisabled) {
|
||||||
|
// Navigate to MetadataScreen
|
||||||
|
if (item.type === 'series' && item.season && item.episode) {
|
||||||
|
const episodeId = `${item.id}:${item.season}:${item.episode}`;
|
||||||
|
navigation.navigate('Metadata', {
|
||||||
|
id: item.id,
|
||||||
|
type: item.type,
|
||||||
|
episodeId: episodeId
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
navigation.navigate('Metadata', {
|
||||||
|
id: item.id,
|
||||||
|
type: item.type
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Navigate to StreamsScreen
|
||||||
|
if (item.type === 'series' && item.season && item.episode) {
|
||||||
|
const episodeId = `${item.id}:${item.season}:${item.episode}`;
|
||||||
|
navigation.navigate('Streams', {
|
||||||
|
id: item.id,
|
||||||
|
type: item.type,
|
||||||
|
episodeId: episodeId
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
navigation.navigate('Streams', {
|
||||||
|
id: item.id,
|
||||||
|
type: item.type
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we have a cached stream for this content
|
// Check if we have a cached stream for this content
|
||||||
const episodeId = item.type === 'series' && item.season && item.episode
|
const episodeId = item.type === 'series' && item.season && item.episode
|
||||||
? `${item.id}:${item.season}:${item.episode}`
|
? `${item.id}:${item.season}:${item.episode}`
|
||||||
|
|
@ -730,7 +771,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [navigation]);
|
}, [navigation, settings.useCachedStreams, settings.openMetadataScreenWhenCacheDisabled]);
|
||||||
|
|
||||||
// Handle long press to delete (moved before renderContinueWatchingItem)
|
// Handle long press to delete (moved before renderContinueWatchingItem)
|
||||||
const handleLongPress = useCallback((item: ContinueWatchingItem) => {
|
const handleLongPress = useCallback((item: ContinueWatchingItem) => {
|
||||||
|
|
|
||||||
|
|
@ -335,7 +335,7 @@ const ActionButtons = memo(({
|
||||||
return isWatched ? 'Play' : playButtonText;
|
return isWatched ? 'Play' : playButtonText;
|
||||||
}, [isWatched, playButtonText, type, watchProgress, groupedEpisodes]);
|
}, [isWatched, playButtonText, type, watchProgress, groupedEpisodes]);
|
||||||
|
|
||||||
// Determine if we should show buttons in a single row (Play, Save, and one other button = 3 total)
|
// Determine if we should show buttons in a single row (Play, Save, and optionally one other button)
|
||||||
const hasAiChat = aiChatEnabled;
|
const hasAiChat = aiChatEnabled;
|
||||||
const hasTraktCollection = isAuthenticated;
|
const hasTraktCollection = isAuthenticated;
|
||||||
const hasRatings = type === 'series';
|
const hasRatings = type === 'series';
|
||||||
|
|
@ -343,16 +343,20 @@ const ActionButtons = memo(({
|
||||||
// Count additional buttons (excluding Play and Save)
|
// Count additional buttons (excluding Play and Save)
|
||||||
const additionalButtonCount = (hasAiChat ? 1 : 0) + (hasTraktCollection ? 1 : 0) + (hasRatings ? 1 : 0);
|
const additionalButtonCount = (hasAiChat ? 1 : 0) + (hasTraktCollection ? 1 : 0) + (hasRatings ? 1 : 0);
|
||||||
|
|
||||||
// Show single row when there's exactly 1 additional button (3 total buttons)
|
// Show single row when there are 0 additional buttons (2 total: Play + Save) or 1 additional button (3 total)
|
||||||
const shouldShowSingleRow = additionalButtonCount === 1;
|
const shouldShowSingleRow = additionalButtonCount <= 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View style={[isTablet ? styles.tabletActionButtons : styles.actionButtons, animatedStyle]}>
|
<Animated.View style={[isTablet ? styles.tabletActionButtons : styles.actionButtons, animatedStyle]}>
|
||||||
{shouldShowSingleRow ? (
|
{shouldShowSingleRow ? (
|
||||||
/* Single Row Layout - Play, Save, and one other button (3 total) */
|
/* Single Row Layout - Play, Save, and optionally one other button (2-3 total) */
|
||||||
<View style={styles.singleRowLayout}>
|
<View style={styles.singleRowLayout}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[playButtonStyle, isTablet && styles.tabletPlayButton, styles.singleRowPlayButton]}
|
style={[
|
||||||
|
playButtonStyle,
|
||||||
|
isTablet && styles.tabletPlayButton,
|
||||||
|
additionalButtonCount === 0 ? styles.singleRowPlayButtonFullWidth : styles.singleRowPlayButton
|
||||||
|
]}
|
||||||
onPress={handleShowStreams}
|
onPress={handleShowStreams}
|
||||||
activeOpacity={0.85}
|
activeOpacity={0.85}
|
||||||
>
|
>
|
||||||
|
|
@ -370,7 +374,12 @@ const ActionButtons = memo(({
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.actionButton, styles.infoButton, isTablet && styles.tabletInfoButton, styles.singleRowSaveButton]}
|
style={[
|
||||||
|
styles.actionButton,
|
||||||
|
styles.infoButton,
|
||||||
|
isTablet && styles.tabletInfoButton,
|
||||||
|
additionalButtonCount === 0 ? styles.singleRowSaveButtonFullWidth : styles.singleRowSaveButton
|
||||||
|
]}
|
||||||
onPress={handleSaveAction}
|
onPress={handleSaveAction}
|
||||||
activeOpacity={0.85}
|
activeOpacity={0.85}
|
||||||
>
|
>
|
||||||
|
|
@ -396,8 +405,8 @@ const ActionButtons = memo(({
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{/* Third Button - AI Chat, Trakt Collection, or Ratings */}
|
{/* Third Button - AI Chat, Trakt Collection, or Ratings (only if available) */}
|
||||||
{hasAiChat && (
|
{hasAiChat && additionalButtonCount === 1 && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.iconButton, isTablet && styles.tabletIconButton, styles.singleRowIconButton]}
|
style={[styles.iconButton, isTablet && styles.tabletIconButton, styles.singleRowIconButton]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
|
|
@ -444,7 +453,7 @@ const ActionButtons = memo(({
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hasTraktCollection && !hasAiChat && (
|
{hasTraktCollection && !hasAiChat && additionalButtonCount === 1 && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.iconButton, isTablet && styles.tabletIconButton, styles.singleRowIconButton]}
|
style={[styles.iconButton, isTablet && styles.tabletIconButton, styles.singleRowIconButton]}
|
||||||
onPress={handleCollectionAction}
|
onPress={handleCollectionAction}
|
||||||
|
|
@ -470,7 +479,7 @@ const ActionButtons = memo(({
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hasRatings && !hasAiChat && !hasTraktCollection && (
|
{hasRatings && !hasAiChat && !hasTraktCollection && additionalButtonCount === 1 && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.iconButton, isTablet && styles.tabletIconButton, styles.singleRowIconButton]}
|
style={[styles.iconButton, isTablet && styles.tabletIconButton, styles.singleRowIconButton]}
|
||||||
onPress={handleRatingsPress}
|
onPress={handleRatingsPress}
|
||||||
|
|
@ -2152,6 +2161,14 @@ const styles = StyleSheet.create({
|
||||||
borderRadius: isTablet ? 25 : 22,
|
borderRadius: isTablet ? 25 : 22,
|
||||||
flex: 0,
|
flex: 0,
|
||||||
},
|
},
|
||||||
|
singleRowPlayButtonFullWidth: {
|
||||||
|
flex: 1,
|
||||||
|
marginHorizontal: 4,
|
||||||
|
},
|
||||||
|
singleRowSaveButtonFullWidth: {
|
||||||
|
flex: 1,
|
||||||
|
marginHorizontal: 4,
|
||||||
|
},
|
||||||
primaryActionRow: {
|
primaryActionRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
gap: 12,
|
gap: 12,
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,9 @@ export interface AppSettings {
|
||||||
useTmdbLocalizedMetadata: boolean; // Use TMDB localized metadata (titles, overviews) per tmdbLanguagePreference
|
useTmdbLocalizedMetadata: boolean; // Use TMDB localized metadata (titles, overviews) per tmdbLanguagePreference
|
||||||
// Trakt integration
|
// Trakt integration
|
||||||
showTraktComments: boolean; // Show Trakt comments in metadata screens
|
showTraktComments: boolean; // Show Trakt comments in metadata screens
|
||||||
|
// Continue Watching behavior
|
||||||
|
useCachedStreams: boolean; // Enable/disable direct player navigation from Continue Watching cache
|
||||||
|
openMetadataScreenWhenCacheDisabled: boolean; // When cache disabled, open MetadataScreen instead of StreamsScreen
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_SETTINGS: AppSettings = {
|
export const DEFAULT_SETTINGS: AppSettings = {
|
||||||
|
|
@ -137,6 +140,9 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
||||||
useTmdbLocalizedMetadata: false,
|
useTmdbLocalizedMetadata: false,
|
||||||
// Trakt integration
|
// Trakt integration
|
||||||
showTraktComments: true, // Show Trakt comments by default when authenticated
|
showTraktComments: true, // Show Trakt comments by default when authenticated
|
||||||
|
// Continue Watching behavior
|
||||||
|
useCachedStreams: false, // Enable by default
|
||||||
|
openMetadataScreenWhenCacheDisabled: true, // Default to StreamsScreen when cache disabled
|
||||||
};
|
};
|
||||||
|
|
||||||
const SETTINGS_STORAGE_KEY = 'app_settings';
|
const SETTINGS_STORAGE_KEY = 'app_settings';
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ import AISettingsScreen from '../screens/AISettingsScreen';
|
||||||
import AIChatScreen from '../screens/AIChatScreen';
|
import AIChatScreen from '../screens/AIChatScreen';
|
||||||
import BackdropGalleryScreen from '../screens/BackdropGalleryScreen';
|
import BackdropGalleryScreen from '../screens/BackdropGalleryScreen';
|
||||||
import BackupScreen from '../screens/BackupScreen';
|
import BackupScreen from '../screens/BackupScreen';
|
||||||
|
import ContinueWatchingSettingsScreen from '../screens/ContinueWatchingSettingsScreen';
|
||||||
|
|
||||||
// Stack navigator types
|
// Stack navigator types
|
||||||
export type RootStackParamList = {
|
export type RootStackParamList = {
|
||||||
|
|
@ -173,6 +174,7 @@ export type RootStackParamList = {
|
||||||
type: 'movie' | 'tv';
|
type: 'movie' | 'tv';
|
||||||
title: string;
|
title: string;
|
||||||
};
|
};
|
||||||
|
ContinueWatchingSettings: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RootStackNavigationProp = NativeStackNavigationProp<RootStackParamList>;
|
export type RootStackNavigationProp = NativeStackNavigationProp<RootStackParamList>;
|
||||||
|
|
@ -1266,6 +1268,21 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="ContinueWatchingSettings"
|
||||||
|
component={ContinueWatchingSettingsScreen}
|
||||||
|
options={{
|
||||||
|
animation: Platform.OS === 'android' ? 'slide_from_right' : 'default',
|
||||||
|
animationDuration: Platform.OS === 'android' ? 250 : 200,
|
||||||
|
presentation: 'card',
|
||||||
|
gestureEnabled: true,
|
||||||
|
gestureDirection: 'horizontal',
|
||||||
|
headerShown: false,
|
||||||
|
contentStyle: {
|
||||||
|
backgroundColor: currentTheme.colors.darkBackground,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="HeroCatalogs"
|
name="HeroCatalogs"
|
||||||
component={HeroCatalogsScreen}
|
component={HeroCatalogsScreen}
|
||||||
|
|
|
||||||
309
src/screens/ContinueWatchingSettingsScreen.tsx
Normal file
|
|
@ -0,0 +1,309 @@
|
||||||
|
import React, { useState, useCallback, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
TouchableOpacity,
|
||||||
|
ScrollView,
|
||||||
|
StatusBar,
|
||||||
|
Platform,
|
||||||
|
Switch,
|
||||||
|
Animated,
|
||||||
|
} from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import { NavigationProp } from '@react-navigation/native';
|
||||||
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
|
import { useSettings } from '../hooks/useSettings';
|
||||||
|
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||||
|
|
||||||
|
const ContinueWatchingSettingsScreen: React.FC = () => {
|
||||||
|
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||||
|
const { settings, updateSetting } = useSettings();
|
||||||
|
const { currentTheme } = useTheme();
|
||||||
|
const colors = currentTheme.colors;
|
||||||
|
const [showSavedIndicator, setShowSavedIndicator] = useState(false);
|
||||||
|
const fadeAnim = React.useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
|
// Prevent iOS entrance flicker by restoring a non-translucent StatusBar
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
StatusBar.setTranslucent(false);
|
||||||
|
StatusBar.setBackgroundColor(colors.darkBackground);
|
||||||
|
StatusBar.setBarStyle('light-content');
|
||||||
|
if (Platform.OS === 'ios') {
|
||||||
|
StatusBar.setHidden(false);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}, [colors.darkBackground]);
|
||||||
|
|
||||||
|
const handleBack = useCallback(() => {
|
||||||
|
navigation.goBack();
|
||||||
|
}, [navigation]);
|
||||||
|
|
||||||
|
// Fade in/out animation for the "Changes saved" indicator
|
||||||
|
useEffect(() => {
|
||||||
|
if (showSavedIndicator) {
|
||||||
|
Animated.sequence([
|
||||||
|
Animated.timing(fadeAnim, {
|
||||||
|
toValue: 1,
|
||||||
|
duration: 300,
|
||||||
|
useNativeDriver: true
|
||||||
|
}),
|
||||||
|
Animated.delay(1000),
|
||||||
|
Animated.timing(fadeAnim, {
|
||||||
|
toValue: 0,
|
||||||
|
duration: 300,
|
||||||
|
useNativeDriver: true
|
||||||
|
})
|
||||||
|
]).start(() => setShowSavedIndicator(false));
|
||||||
|
}
|
||||||
|
}, [showSavedIndicator, fadeAnim]);
|
||||||
|
|
||||||
|
const handleUpdateSetting = useCallback(<K extends keyof typeof settings>(
|
||||||
|
key: K,
|
||||||
|
value: typeof settings[K]
|
||||||
|
) => {
|
||||||
|
updateSetting(key, value);
|
||||||
|
setShowSavedIndicator(true);
|
||||||
|
}, [updateSetting]);
|
||||||
|
|
||||||
|
const CustomSwitch = ({ value, onValueChange }: { value: boolean; onValueChange: (value: boolean) => void }) => (
|
||||||
|
<Switch
|
||||||
|
value={value}
|
||||||
|
onValueChange={onValueChange}
|
||||||
|
trackColor={{ false: colors.elevation2, true: colors.primary }}
|
||||||
|
thumbColor={value ? colors.white : colors.mediumEmphasis}
|
||||||
|
ios_backgroundColor={colors.elevation2}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const SettingItem = ({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
value,
|
||||||
|
onValueChange,
|
||||||
|
isLast = false
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
value: boolean;
|
||||||
|
onValueChange: (value: boolean) => void;
|
||||||
|
isLast?: boolean;
|
||||||
|
}) => (
|
||||||
|
<View style={[
|
||||||
|
styles.settingItem,
|
||||||
|
{
|
||||||
|
borderBottomColor: isLast ? 'transparent' : colors.border,
|
||||||
|
backgroundColor: colors.elevation1
|
||||||
|
}
|
||||||
|
]}>
|
||||||
|
<View style={styles.settingContent}>
|
||||||
|
<Text style={[styles.settingTitle, { color: colors.highEmphasis }]}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Text style={[styles.settingDescription, { color: colors.mediumEmphasis }]}>
|
||||||
|
{description}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<CustomSwitch value={value} onValueChange={onValueChange} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
const SectionHeader = ({ title }: { title: string }) => (
|
||||||
|
<View style={[styles.sectionHeader, { backgroundColor: colors.darkBackground }]}>
|
||||||
|
<Text style={[styles.sectionTitle, { color: colors.highEmphasis }]}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={[styles.container, { backgroundColor: colors.darkBackground }]}>
|
||||||
|
<StatusBar barStyle="light-content" />
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<View style={[styles.header, { backgroundColor: colors.darkBackground }]}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.backButton}
|
||||||
|
onPress={handleBack}
|
||||||
|
>
|
||||||
|
<MaterialIcons name="chevron-left" size={28} color={colors.primary} />
|
||||||
|
<Text style={[styles.backText, { color: colors.primary }]}>Settings</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={[styles.headerTitle, { color: colors.highEmphasis }]}>
|
||||||
|
Continue Watching
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<ScrollView
|
||||||
|
style={styles.content}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
contentContainerStyle={styles.contentContainer}
|
||||||
|
>
|
||||||
|
<SectionHeader title="PLAYBACK BEHAVIOR" />
|
||||||
|
|
||||||
|
<View style={[styles.settingsCard, { backgroundColor: colors.elevation1 }]}>
|
||||||
|
<SettingItem
|
||||||
|
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."
|
||||||
|
value={settings.useCachedStreams}
|
||||||
|
onValueChange={(value) => handleUpdateSetting('useCachedStreams', value)}
|
||||||
|
isLast={!settings.useCachedStreams}
|
||||||
|
/>
|
||||||
|
{!settings.useCachedStreams && (
|
||||||
|
<SettingItem
|
||||||
|
title="Open Metadata Screen"
|
||||||
|
description="When cached streams are disabled, open the Metadata screen instead of the Streams screen. This shows content details and allows manual stream selection."
|
||||||
|
value={settings.openMetadataScreenWhenCacheDisabled}
|
||||||
|
onValueChange={(value) => handleUpdateSetting('openMetadataScreenWhenCacheDisabled', value)}
|
||||||
|
isLast={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={[styles.infoCard, { backgroundColor: colors.elevation1 }]}>
|
||||||
|
<View style={styles.infoHeader}>
|
||||||
|
<MaterialIcons name="info" size={20} color={colors.primary} />
|
||||||
|
<Text style={[styles.infoTitle, { color: colors.highEmphasis }]}>
|
||||||
|
How it works
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={[styles.infoText, { color: colors.mediumEmphasis }]}>
|
||||||
|
• 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
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* Saved indicator */}
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.savedIndicator,
|
||||||
|
{
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
opacity: fadeAnim
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<MaterialIcons name="check" size={20} color={colors.white} />
|
||||||
|
<Text style={styles.savedText}>Changes saved</Text>
|
||||||
|
</Animated.View>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingTop: Platform.OS === 'ios' ? 0 : 12,
|
||||||
|
},
|
||||||
|
backButton: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginRight: 16,
|
||||||
|
},
|
||||||
|
backText: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
marginLeft: 4,
|
||||||
|
},
|
||||||
|
headerTitle: {
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: '700',
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
contentContainer: {
|
||||||
|
paddingBottom: 100,
|
||||||
|
},
|
||||||
|
sectionHeader: {
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingTop: 24,
|
||||||
|
},
|
||||||
|
sectionTitle: {
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: '700',
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
},
|
||||||
|
settingsCard: {
|
||||||
|
marginHorizontal: 16,
|
||||||
|
borderRadius: 12,
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
settingItem: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 16,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
},
|
||||||
|
settingContent: {
|
||||||
|
flex: 1,
|
||||||
|
marginRight: 16,
|
||||||
|
},
|
||||||
|
settingTitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
settingDescription: {
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: 20,
|
||||||
|
},
|
||||||
|
infoCard: {
|
||||||
|
marginHorizontal: 16,
|
||||||
|
marginTop: 16,
|
||||||
|
padding: 16,
|
||||||
|
borderRadius: 12,
|
||||||
|
},
|
||||||
|
infoHeader: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
infoTitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
marginLeft: 8,
|
||||||
|
},
|
||||||
|
infoText: {
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: 20,
|
||||||
|
},
|
||||||
|
savedIndicator: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 32,
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
savedText: {
|
||||||
|
color: '#FFFFFF',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '600',
|
||||||
|
marginLeft: 8,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ContinueWatchingSettingsScreen;
|
||||||
|
|
@ -483,6 +483,14 @@ const SettingsScreen: React.FC = () => {
|
||||||
icon="home"
|
icon="home"
|
||||||
renderControl={ChevronRight}
|
renderControl={ChevronRight}
|
||||||
onPress={() => navigation.navigate('HomeScreenSettings')}
|
onPress={() => navigation.navigate('HomeScreenSettings')}
|
||||||
|
isTablet={isTablet}
|
||||||
|
/>
|
||||||
|
<SettingItem
|
||||||
|
title="Continue Watching"
|
||||||
|
description="Cache and playback behavior"
|
||||||
|
icon="play-circle"
|
||||||
|
renderControl={ChevronRight}
|
||||||
|
onPress={() => navigation.navigate('ContinueWatchingSettings')}
|
||||||
isLast={true}
|
isLast={true}
|
||||||
isTablet={isTablet}
|
isTablet={isTablet}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1556,7 +1556,7 @@ export const StreamsScreen = () => {
|
||||||
];
|
];
|
||||||
}, [availableProviders, type, episodeStreams, groupedStreams, settings.streamDisplayMode]);
|
}, [availableProviders, type, episodeStreams, groupedStreams, settings.streamDisplayMode]);
|
||||||
|
|
||||||
const sections = useMemo(() => {
|
const sections: Array<{ title: string; addonId: string; data: Stream[]; isEmptyDueToQualityFilter?: boolean } | null> = useMemo(() => {
|
||||||
const streams = metadata?.videos && metadata.videos.length > 1 && selectedEpisode ? episodeStreams : groupedStreams;
|
const streams = metadata?.videos && metadata.videos.length > 1 && selectedEpisode ? episodeStreams : groupedStreams;
|
||||||
const installedAddons = stremioService.getInstalledAddons();
|
const installedAddons = stremioService.getInstalledAddons();
|
||||||
|
|
||||||
|
|
@ -1648,12 +1648,7 @@ export const StreamsScreen = () => {
|
||||||
const isEmptyDueToQualityFilter = totalOriginalCount > 0 && totalStreamsCount === 0;
|
const isEmptyDueToQualityFilter = totalOriginalCount > 0 && totalStreamsCount === 0;
|
||||||
|
|
||||||
if (isEmptyDueToQualityFilter) {
|
if (isEmptyDueToQualityFilter) {
|
||||||
return [{
|
return []; // Return empty array instead of showing placeholder
|
||||||
title: 'Available Streams',
|
|
||||||
addonId: 'grouped-all',
|
|
||||||
data: [{ isEmptyPlaceholder: true } as any],
|
|
||||||
isEmptyDueToQualityFilter: true
|
|
||||||
}];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine streams: Addons first (unsorted), then sorted plugins
|
// Combine streams: Addons first (unsorted), then sorted plugins
|
||||||
|
|
@ -1780,12 +1775,7 @@ export const StreamsScreen = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEmptyDueToQualityFilter) {
|
if (isEmptyDueToQualityFilter) {
|
||||||
return {
|
return null; // Return null to exclude this section completely
|
||||||
title: addonName,
|
|
||||||
addonId,
|
|
||||||
data: [{ isEmptyPlaceholder: true } as any],
|
|
||||||
isEmptyDueToQualityFilter
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let processedStreams = filteredStreams;
|
let processedStreams = filteredStreams;
|
||||||
|
|
@ -1861,7 +1851,7 @@ export const StreamsScreen = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
});
|
}).filter(Boolean); // Filter out null values
|
||||||
}
|
}
|
||||||
}, [selectedProvider, type, episodeStreams, groupedStreams, settings.streamDisplayMode, filterStreamsByQuality, addonResponseOrder, settings.streamSortMode, selectedEpisode, metadata]);
|
}, [selectedProvider, type, episodeStreams, groupedStreams, settings.streamDisplayMode, filterStreamsByQuality, addonResponseOrder, settings.streamSortMode, selectedEpisode, metadata]);
|
||||||
|
|
||||||
|
|
@ -1869,11 +1859,11 @@ export const StreamsScreen = () => {
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
console.log('🔍 [StreamsScreen] Final sections:', {
|
console.log('🔍 [StreamsScreen] Final sections:', {
|
||||||
sectionsCount: sections.length,
|
sectionsCount: sections.length,
|
||||||
sections: sections.map(s => ({
|
sections: sections.filter(Boolean).map(s => ({
|
||||||
title: s.title,
|
title: s!.title,
|
||||||
addonId: s.addonId,
|
addonId: s!.addonId,
|
||||||
dataCount: s.data?.length || 0,
|
dataCount: s!.data?.length || 0,
|
||||||
isEmptyDueToQualityFilter: s.isEmptyDueToQualityFilter
|
isEmptyDueToQualityFilter: s!.isEmptyDueToQualityFilter
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
}, [sections]);
|
}, [sections]);
|
||||||
|
|
@ -2206,15 +2196,15 @@ export const StreamsScreen = () => {
|
||||||
scrollEventThrottle: 16,
|
scrollEventThrottle: 16,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{sections.map((section, sectionIndex) => (
|
{sections.filter(Boolean).map((section, sectionIndex) => (
|
||||||
<View key={section.addonId || sectionIndex}>
|
<View key={section!.addonId || sectionIndex}>
|
||||||
{/* Section Header */}
|
{/* Section Header */}
|
||||||
{renderSectionHeader({ section })}
|
{renderSectionHeader({ section: section! })}
|
||||||
|
|
||||||
{/* Stream Cards using FlatList */}
|
{/* Stream Cards using FlatList */}
|
||||||
{section.data && section.data.length > 0 ? (
|
{section!.data && section!.data.length > 0 ? (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={section.data}
|
data={section!.data}
|
||||||
keyExtractor={(item, index) => {
|
keyExtractor={(item, index) => {
|
||||||
if (item && item.url) {
|
if (item && item.url) {
|
||||||
return `${item.url}-${sectionIndex}-${index}`;
|
return `${item.url}-${sectionIndex}-${index}`;
|
||||||
|
|
@ -2257,20 +2247,7 @@ export const StreamsScreen = () => {
|
||||||
index,
|
index,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : null}
|
||||||
// Empty section placeholder
|
|
||||||
<View style={styles.emptySectionContainer}>
|
|
||||||
<View style={styles.emptySectionContent}>
|
|
||||||
<MaterialIcons name="filter-list-off" size={32} color={colors.mediumEmphasis} />
|
|
||||||
<Text style={[styles.emptySectionTitle, { color: colors.mediumEmphasis }]}>
|
|
||||||
No streams available
|
|
||||||
</Text>
|
|
||||||
<Text style={[styles.emptySectionSubtitle, { color: colors.textMuted }]}>
|
|
||||||
All streams were filtered by your quality settings
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|
@ -2805,28 +2782,6 @@ const createStyles = (colors: any) => StyleSheet.create({
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: '400',
|
fontWeight: '400',
|
||||||
},
|
},
|
||||||
emptySectionContainer: {
|
|
||||||
padding: 16,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
minHeight: 80,
|
|
||||||
},
|
|
||||||
emptySectionContent: {
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
emptySectionTitle: {
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: '600',
|
|
||||||
marginTop: 8,
|
|
||||||
textAlign: 'center',
|
|
||||||
},
|
|
||||||
emptySectionSubtitle: {
|
|
||||||
fontSize: 12,
|
|
||||||
marginTop: 4,
|
|
||||||
textAlign: 'center',
|
|
||||||
lineHeight: 16,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default memo(StreamsScreen);
|
export default memo(StreamsScreen);
|
||||||
|
|
|
||||||
|
|
@ -92,14 +92,9 @@ class StreamCacheService {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate that the stream URL is still accessible (quick HEAD request)
|
// Skip URL validation for now - many CDNs block HEAD requests
|
||||||
logger.log(`🔍 [StreamCache] Validating stream URL: ${cacheEntry.cachedStream.url}`);
|
// This was causing valid streams to be rejected
|
||||||
const isUrlValid = await this.validateStreamUrl(cacheEntry.cachedStream.url);
|
logger.log(`🔍 [StreamCache] Skipping URL validation (CDN compatibility)`);
|
||||||
if (!isUrlValid) {
|
|
||||||
logger.log(`❌ [StreamCache] Stream URL invalid for ${type}:${id}${episodeId ? `:${episodeId}` : ''}`);
|
|
||||||
await this.removeCachedStream(id, type, episodeId);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.log(`✅ [StreamCache] Using cached stream for ${type}:${id}${episodeId ? `:${episodeId}` : ''}`);
|
logger.log(`✅ [StreamCache] Using cached stream for ${type}:${id}${episodeId ? `:${episodeId}` : ''}`);
|
||||||
return cacheEntry.cachedStream;
|
return cacheEntry.cachedStream;
|
||||||
|
|
@ -131,7 +126,7 @@ class StreamCacheService {
|
||||||
const cacheKeys = allKeys.filter(key => key.startsWith(CACHE_KEY_PREFIX));
|
const cacheKeys = allKeys.filter(key => key.startsWith(CACHE_KEY_PREFIX));
|
||||||
|
|
||||||
for (const key of cacheKeys) {
|
for (const key of cacheKeys) {
|
||||||
await storageService.removeItem(key);
|
await AsyncStorage.removeItem(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log(`🧹 [StreamCache] Cleared ${cacheKeys.length} cached streams`);
|
logger.log(`🧹 [StreamCache] Cleared ${cacheKeys.length} cached streams`);
|
||||||
|
|
@ -173,8 +168,8 @@ class StreamCacheService {
|
||||||
*/
|
*/
|
||||||
async getCacheInfo(): Promise<{ totalCached: number; expiredCount: number; validCount: number }> {
|
async getCacheInfo(): Promise<{ totalCached: number; expiredCount: number; validCount: number }> {
|
||||||
try {
|
try {
|
||||||
const allKeys = await storageService.getAllKeys();
|
const allKeys = await AsyncStorage.getAllKeys();
|
||||||
const cacheKeys = allKeys.filter(key => key.startsWith(CACHE_KEY_PREFIX));
|
const cacheKeys = allKeys.filter((key: string) => key.startsWith(CACHE_KEY_PREFIX));
|
||||||
|
|
||||||
let expiredCount = 0;
|
let expiredCount = 0;
|
||||||
let validCount = 0;
|
let validCount = 0;
|
||||||
|
|
@ -182,7 +177,7 @@ class StreamCacheService {
|
||||||
|
|
||||||
for (const key of cacheKeys) {
|
for (const key of cacheKeys) {
|
||||||
try {
|
try {
|
||||||
const cachedData = await storageService.getItem(key);
|
const cachedData = await AsyncStorage.getItem(key);
|
||||||
if (cachedData) {
|
if (cachedData) {
|
||||||
const cacheEntry: StreamCacheEntry = JSON.parse(cachedData);
|
const cacheEntry: StreamCacheEntry = JSON.parse(cachedData);
|
||||||
if (now > cacheEntry.expiresAt) {
|
if (now > cacheEntry.expiresAt) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Single source of truth for the app version displayed in Settings
|
// Single source of truth for the app version displayed in Settings
|
||||||
// Update this when bumping app version
|
// Update this when bumping app version
|
||||||
|
|
||||||
export const APP_VERSION = '1.2.5';
|
export const APP_VERSION = '1.2.6';
|
||||||
|
|
||||||
export function getDisplayedAppVersion(): string {
|
export function getDisplayedAppVersion(): string {
|
||||||
return APP_VERSION;
|
return APP_VERSION;
|
||||||
|
|
|
||||||