continue watching changes

This commit is contained in:
tapframe 2025-10-21 16:09:35 +05:30
parent 373efa0564
commit 1ae6b4f108
23 changed files with 444 additions and 96 deletions

View file

@ -94,8 +94,8 @@ android {
applicationId 'com.nuvio.app'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 20
versionName "1.2.5"
versionCode 21
versionName "1.2.6"
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]
applicationVariants.all { variant ->
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 versionCode = baseVersionCode * 100 // Base multiplier

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 173 KiB

View file

@ -3,5 +3,5 @@
<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_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>

View file

@ -2,7 +2,7 @@
"expo": {
"name": "Nuvio",
"slug": "nuvio",
"version": "1.2.5",
"version": "1.2.6",
"orientation": "default",
"backgroundColor": "#020404",
"icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png",
@ -17,7 +17,7 @@
"ios": {
"supportsTablet": true,
"icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png",
"buildNumber": "20",
"buildNumber": "21",
"infoPlist": {
"NSAppTransportSecurity": {
"NSAllowsArbitraryLoads": true
@ -48,7 +48,7 @@
"WAKE_LOCK"
],
"package": "com.nuvio.app",
"versionCode": 20,
"versionCode": 21,
"architectures": [
"arm64-v8a",
"armeabi-v7a",
@ -95,6 +95,6 @@
"fallbackToCacheTimeout": 30000,
"url": "https://grim-reyna-tapframe-69970143.koyeb.app/api/manifest"
},
"runtimeVersion": "1.2.5"
"runtimeVersion": "1.2.6"
}
}

View file

@ -460,7 +460,7 @@
"-lc++",
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = "com.nuvio.app";
PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app;
PRODUCT_NAME = "Nuvio";
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View file

@ -19,7 +19,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.2.5</string>
<string>1.2.6</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@ -39,7 +39,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>20</string>
<string>21</string>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
<key>LSRequiresIPhoneOS</key>

View file

@ -9,7 +9,7 @@
<key>EXUpdatesLaunchWaitMs</key>
<integer>30000</integer>
<key>EXUpdatesRuntimeVersion</key>
<string>1.2.5</string>
<string>1.2.6</string>
<key>EXUpdatesURL</key>
<string>https://grim-reyna-tapframe-69970143.koyeb.app/api/manifest</string>
</dict>

View file

@ -25,6 +25,7 @@ import * as Haptics from 'expo-haptics';
import { TraktService } from '../../services/traktService';
import { stremioService } from '../../services/stremioService';
import { streamCacheService } from '../../services/streamCacheService';
import { useSettings } from '../../hooks/useSettings';
import CustomAlert from '../../components/CustomAlert';
// Define interface for continue watching items
@ -99,6 +100,7 @@ const isEpisodeReleased = (video: any): boolean => {
const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, ref) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme();
const { settings } = useSettings();
const [continueWatchingItems, setContinueWatchingItems] = useState<ContinueWatchingItem[]>([]);
const [loading, setLoading] = useState(true);
const appState = useRef(AppState.currentState);
@ -656,6 +658,45 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
try {
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
const episodeId = item.type === 'series' && 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)
const handleLongPress = useCallback((item: ContinueWatchingItem) => {

View file

@ -335,7 +335,7 @@ const ActionButtons = memo(({
return isWatched ? 'Play' : playButtonText;
}, [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 hasTraktCollection = isAuthenticated;
const hasRatings = type === 'series';
@ -343,16 +343,20 @@ const ActionButtons = memo(({
// Count additional buttons (excluding Play and Save)
const additionalButtonCount = (hasAiChat ? 1 : 0) + (hasTraktCollection ? 1 : 0) + (hasRatings ? 1 : 0);
// Show single row when there's exactly 1 additional button (3 total buttons)
const shouldShowSingleRow = additionalButtonCount === 1;
// Show single row when there are 0 additional buttons (2 total: Play + Save) or 1 additional button (3 total)
const shouldShowSingleRow = additionalButtonCount <= 1;
return (
<Animated.View style={[isTablet ? styles.tabletActionButtons : styles.actionButtons, animatedStyle]}>
{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}>
<TouchableOpacity
style={[playButtonStyle, isTablet && styles.tabletPlayButton, styles.singleRowPlayButton]}
style={[
playButtonStyle,
isTablet && styles.tabletPlayButton,
additionalButtonCount === 0 ? styles.singleRowPlayButtonFullWidth : styles.singleRowPlayButton
]}
onPress={handleShowStreams}
activeOpacity={0.85}
>
@ -370,7 +374,12 @@ const ActionButtons = memo(({
</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}
activeOpacity={0.85}
>
@ -396,8 +405,8 @@ const ActionButtons = memo(({
</Text>
</TouchableOpacity>
{/* Third Button - AI Chat, Trakt Collection, or Ratings */}
{hasAiChat && (
{/* Third Button - AI Chat, Trakt Collection, or Ratings (only if available) */}
{hasAiChat && additionalButtonCount === 1 && (
<TouchableOpacity
style={[styles.iconButton, isTablet && styles.tabletIconButton, styles.singleRowIconButton]}
onPress={() => {
@ -444,7 +453,7 @@ const ActionButtons = memo(({
</TouchableOpacity>
)}
{hasTraktCollection && !hasAiChat && (
{hasTraktCollection && !hasAiChat && additionalButtonCount === 1 && (
<TouchableOpacity
style={[styles.iconButton, isTablet && styles.tabletIconButton, styles.singleRowIconButton]}
onPress={handleCollectionAction}
@ -470,7 +479,7 @@ const ActionButtons = memo(({
</TouchableOpacity>
)}
{hasRatings && !hasAiChat && !hasTraktCollection && (
{hasRatings && !hasAiChat && !hasTraktCollection && additionalButtonCount === 1 && (
<TouchableOpacity
style={[styles.iconButton, isTablet && styles.tabletIconButton, styles.singleRowIconButton]}
onPress={handleRatingsPress}
@ -2152,6 +2161,14 @@ const styles = StyleSheet.create({
borderRadius: isTablet ? 25 : 22,
flex: 0,
},
singleRowPlayButtonFullWidth: {
flex: 1,
marginHorizontal: 4,
},
singleRowSaveButtonFullWidth: {
flex: 1,
marginHorizontal: 4,
},
primaryActionRow: {
flexDirection: 'row',
gap: 12,

View file

@ -82,6 +82,9 @@ export interface AppSettings {
useTmdbLocalizedMetadata: boolean; // Use TMDB localized metadata (titles, overviews) per tmdbLanguagePreference
// Trakt integration
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 = {
@ -137,6 +140,9 @@ export const DEFAULT_SETTINGS: AppSettings = {
useTmdbLocalizedMetadata: false,
// Trakt integration
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';

View file

@ -67,6 +67,7 @@ import AISettingsScreen from '../screens/AISettingsScreen';
import AIChatScreen from '../screens/AIChatScreen';
import BackdropGalleryScreen from '../screens/BackdropGalleryScreen';
import BackupScreen from '../screens/BackupScreen';
import ContinueWatchingSettingsScreen from '../screens/ContinueWatchingSettingsScreen';
// Stack navigator types
export type RootStackParamList = {
@ -173,6 +174,7 @@ export type RootStackParamList = {
type: 'movie' | 'tv';
title: string;
};
ContinueWatchingSettings: undefined;
};
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
name="HeroCatalogs"
component={HeroCatalogsScreen}

View 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;

View file

@ -483,6 +483,14 @@ const SettingsScreen: React.FC = () => {
icon="home"
renderControl={ChevronRight}
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}
isTablet={isTablet}
/>

View file

@ -1556,7 +1556,7 @@ export const StreamsScreen = () => {
];
}, [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 installedAddons = stremioService.getInstalledAddons();
@ -1648,12 +1648,7 @@ export const StreamsScreen = () => {
const isEmptyDueToQualityFilter = totalOriginalCount > 0 && totalStreamsCount === 0;
if (isEmptyDueToQualityFilter) {
return [{
title: 'Available Streams',
addonId: 'grouped-all',
data: [{ isEmptyPlaceholder: true } as any],
isEmptyDueToQualityFilter: true
}];
return []; // Return empty array instead of showing placeholder
}
// Combine streams: Addons first (unsorted), then sorted plugins
@ -1780,12 +1775,7 @@ export const StreamsScreen = () => {
}
if (isEmptyDueToQualityFilter) {
return {
title: addonName,
addonId,
data: [{ isEmptyPlaceholder: true } as any],
isEmptyDueToQualityFilter
};
return null; // Return null to exclude this section completely
}
let processedStreams = filteredStreams;
@ -1861,7 +1851,7 @@ export const StreamsScreen = () => {
});
return result;
});
}).filter(Boolean); // Filter out null values
}
}, [selectedProvider, type, episodeStreams, groupedStreams, settings.streamDisplayMode, filterStreamsByQuality, addonResponseOrder, settings.streamSortMode, selectedEpisode, metadata]);
@ -1869,11 +1859,11 @@ export const StreamsScreen = () => {
React.useEffect(() => {
console.log('🔍 [StreamsScreen] Final sections:', {
sectionsCount: sections.length,
sections: sections.map(s => ({
title: s.title,
addonId: s.addonId,
dataCount: s.data?.length || 0,
isEmptyDueToQualityFilter: s.isEmptyDueToQualityFilter
sections: sections.filter(Boolean).map(s => ({
title: s!.title,
addonId: s!.addonId,
dataCount: s!.data?.length || 0,
isEmptyDueToQualityFilter: s!.isEmptyDueToQualityFilter
}))
});
}, [sections]);
@ -2206,15 +2196,15 @@ export const StreamsScreen = () => {
scrollEventThrottle: 16,
})}
>
{sections.map((section, sectionIndex) => (
<View key={section.addonId || sectionIndex}>
{sections.filter(Boolean).map((section, sectionIndex) => (
<View key={section!.addonId || sectionIndex}>
{/* Section Header */}
{renderSectionHeader({ section })}
{renderSectionHeader({ section: section! })}
{/* Stream Cards using FlatList */}
{section.data && section.data.length > 0 ? (
{section!.data && section!.data.length > 0 ? (
<FlatList
data={section.data}
data={section!.data}
keyExtractor={(item, index) => {
if (item && item.url) {
return `${item.url}-${sectionIndex}-${index}`;
@ -2257,20 +2247,7 @@ export const StreamsScreen = () => {
index,
})}
/>
) : (
// 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>
)}
) : null}
</View>
))}
@ -2805,28 +2782,6 @@ const createStyles = (colors: any) => StyleSheet.create({
fontSize: 11,
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);

View file

@ -92,14 +92,9 @@ class StreamCacheService {
return null;
}
// Validate that the stream URL is still accessible (quick HEAD request)
logger.log(`🔍 [StreamCache] Validating stream URL: ${cacheEntry.cachedStream.url}`);
const isUrlValid = await this.validateStreamUrl(cacheEntry.cachedStream.url);
if (!isUrlValid) {
logger.log(`❌ [StreamCache] Stream URL invalid for ${type}:${id}${episodeId ? `:${episodeId}` : ''}`);
await this.removeCachedStream(id, type, episodeId);
return null;
}
// Skip URL validation for now - many CDNs block HEAD requests
// This was causing valid streams to be rejected
logger.log(`🔍 [StreamCache] Skipping URL validation (CDN compatibility)`);
logger.log(`✅ [StreamCache] Using cached stream for ${type}:${id}${episodeId ? `:${episodeId}` : ''}`);
return cacheEntry.cachedStream;
@ -131,7 +126,7 @@ class StreamCacheService {
const cacheKeys = allKeys.filter(key => key.startsWith(CACHE_KEY_PREFIX));
for (const key of cacheKeys) {
await storageService.removeItem(key);
await AsyncStorage.removeItem(key);
}
logger.log(`🧹 [StreamCache] Cleared ${cacheKeys.length} cached streams`);
@ -173,8 +168,8 @@ class StreamCacheService {
*/
async getCacheInfo(): Promise<{ totalCached: number; expiredCount: number; validCount: number }> {
try {
const allKeys = await storageService.getAllKeys();
const cacheKeys = allKeys.filter(key => key.startsWith(CACHE_KEY_PREFIX));
const allKeys = await AsyncStorage.getAllKeys();
const cacheKeys = allKeys.filter((key: string) => key.startsWith(CACHE_KEY_PREFIX));
let expiredCount = 0;
let validCount = 0;
@ -182,7 +177,7 @@ class StreamCacheService {
for (const key of cacheKeys) {
try {
const cachedData = await storageService.getItem(key);
const cachedData = await AsyncStorage.getItem(key);
if (cachedData) {
const cacheEntry: StreamCacheEntry = JSON.parse(cachedData);
if (now > cacheEntry.expiresAt) {

View file

@ -1,7 +1,7 @@
// Single source of truth for the app version displayed in Settings
// Update this when bumping app version
export const APP_VERSION = '1.2.5';
export const APP_VERSION = '1.2.6';
export function getDisplayedAppVersion(): string {
return APP_VERSION;