UI changes

This commit is contained in:
tapframe 2025-10-14 02:43:21 +05:30
parent 9e7b9c5fe4
commit 1a9d59e804
6 changed files with 333 additions and 136 deletions

View file

@ -44,13 +44,13 @@ export const CustomAlert = ({
const themeColors = currentTheme.colors; const themeColors = currentTheme.colors;
useEffect(() => { useEffect(() => {
const animDuration = Platform.OS === 'android' ? 200 : 120; const duration = Platform.OS === 'android' ? 200 : 150;
if (visible) { if (visible) {
opacity.value = withTiming(1, { duration: animDuration }); opacity.value = withTiming(1, { duration });
scale.value = withTiming(1, { duration: animDuration }); scale.value = withTiming(1, { duration });
} else { } else {
opacity.value = withTiming(0, { duration: animDuration }); opacity.value = withTiming(0, { duration });
scale.value = withTiming(0.95, { duration: animDuration }); scale.value = withTiming(0.95, { duration });
} }
}, [visible]); }, [visible]);
@ -63,10 +63,6 @@ export const CustomAlert = ({
opacity: opacity.value, opacity: opacity.value,
})); }));
const backgroundColor = isDarkMode ? themeColors.darkBackground : themeColors.elevation2 || '#FFFFFF';
const textColor = isDarkMode ? themeColors.white : themeColors.black || '#000000';
const borderColor = isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)';
// Safe action handler to prevent crashes // Safe action handler to prevent crashes
const handleActionPress = useCallback((action: { label: string; onPress: () => void; style?: object }) => { const handleActionPress = useCallback((action: { label: string; onPress: () => void; style?: object }) => {
try { try {
@ -95,23 +91,54 @@ export const CustomAlert = ({
statusBarTranslucent={false} statusBarTranslucent={false}
hardwareAccelerated={true} hardwareAccelerated={true}
> >
<View style={[styles.overlay, { backgroundColor: 'rgba(0,0,0,0.5)' }]}> <View style={[styles.overlay, { backgroundColor: 'rgba(0,0,0,0.6)' }]}>
<Pressable style={styles.overlayPressable} onPress={onClose} /> <Pressable style={styles.overlayPressable} onPress={onClose} />
<View style={styles.centered}> <View style={styles.centered}>
<View style={[styles.alertContainer, { backgroundColor, borderColor }]}> <View style={[
<Text style={[styles.title, { color: textColor }]}>{title}</Text> styles.alertContainer,
<Text style={[styles.message, { color: textColor }]}>{message}</Text> {
backgroundColor: themeColors.darkBackground,
borderColor: themeColors.primary,
}
]}>
{/* Title */}
<Text style={[styles.title, { color: themeColors.highEmphasis }]}>
{title}
</Text>
{/* Message */}
<Text style={[styles.message, { color: themeColors.mediumEmphasis }]}>
{message}
</Text>
{/* Actions */}
<View style={styles.actionsRow}> <View style={styles.actionsRow}>
{actions.map((action, idx) => ( {actions.map((action, idx) => {
<TouchableOpacity const isPrimary = idx === actions.length - 1;
key={action.label} return (
style={[styles.actionButton, idx === actions.length - 1 && styles.lastActionButton, action.style]} <TouchableOpacity
onPress={() => handleActionPress(action)} key={action.label}
activeOpacity={0.7} style={[
> styles.actionButton,
<Text style={[styles.actionText, { color: themeColors.primary }]}>{action.label}</Text> isPrimary
</TouchableOpacity> ? { ...styles.primaryButton, backgroundColor: themeColors.primary }
))} : styles.secondaryButton,
action.style
]}
onPress={() => handleActionPress(action)}
activeOpacity={0.7}
>
<Text style={[
styles.actionText,
isPrimary
? { color: themeColors.white }
: { color: themeColors.primary }
]}>
{action.label}
</Text>
</TouchableOpacity>
);
})}
</View> </View>
</View> </View>
</View> </View>
@ -129,23 +156,55 @@ export const CustomAlert = ({
onRequestClose={onClose} onRequestClose={onClose}
presentationStyle="overFullScreen" presentationStyle="overFullScreen"
> >
<Animated.View style={[styles.overlay, { backgroundColor: themeColors.transparentDark || 'rgba(0,0,0,0.5)' }, overlayStyle]}> <Animated.View style={[styles.overlay, { backgroundColor: 'rgba(0,0,0,0.6)' }, overlayStyle]}>
<Pressable style={styles.overlayPressable} onPress={onClose} /> <Pressable style={styles.overlayPressable} onPress={onClose} />
<View style={styles.centered}> <View style={styles.centered}>
<Animated.View style={[styles.alertContainer, alertStyle, { backgroundColor, borderColor }]}> <Animated.View style={[
<Text style={[styles.title, { color: textColor }]}>{title}</Text> styles.alertContainer,
<Text style={[styles.message, { color: textColor }]}>{message}</Text> alertStyle,
{
backgroundColor: themeColors.darkBackground,
borderColor: themeColors.primary,
}
]}>
{/* Title */}
<Text style={[styles.title, { color: themeColors.highEmphasis }]}>
{title}
</Text>
{/* Message */}
<Text style={[styles.message, { color: themeColors.mediumEmphasis }]}>
{message}
</Text>
{/* Actions */}
<View style={styles.actionsRow}> <View style={styles.actionsRow}>
{actions.map((action, idx) => ( {actions.map((action, idx) => {
<TouchableOpacity const isPrimary = idx === actions.length - 1;
key={action.label} return (
style={[styles.actionButton, idx === actions.length - 1 && styles.lastActionButton, action.style]} <TouchableOpacity
onPress={() => handleActionPress(action)} key={action.label}
activeOpacity={0.7} style={[
> styles.actionButton,
<Text style={[styles.actionText, { color: themeColors.primary }]}>{action.label}</Text> isPrimary
</TouchableOpacity> ? { ...styles.primaryButton, backgroundColor: themeColors.primary }
))} : styles.secondaryButton,
action.style
]}
onPress={() => handleActionPress(action)}
activeOpacity={0.7}
>
<Text style={[
styles.actionText,
isPrimary
? { color: themeColors.white }
: { color: themeColors.primary }
]}>
{action.label}
</Text>
</TouchableOpacity>
);
})}
</View> </View>
</Animated.View> </Animated.View>
</View> </View>
@ -167,52 +226,66 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
paddingHorizontal: 24,
}, },
alertContainer: { alertContainer: {
minWidth: 280, width: '100%',
maxWidth: '85%', maxWidth: 340,
borderRadius: 20, borderRadius: 24,
padding: 24, padding: 28,
borderWidth: 1, borderWidth: 1,
borderColor: '#007AFF', // iOS blue - will be overridden by theme
overflow: 'hidden', // Ensure background fills entire card
...Platform.select({ ...Platform.select({
ios: { ios: {
shadowColor: '#000', shadowColor: '#000',
shadowOffset: { width: 0, height: 2 }, shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.15, shadowOpacity: 0.3,
shadowRadius: 8, shadowRadius: 24,
}, },
android: { android: {
elevation: 8, elevation: 12,
}, },
}), }),
}, },
title: { title: {
fontSize: 18, fontSize: 20,
fontWeight: '700', fontWeight: '700',
marginBottom: 12, marginBottom: 8,
textAlign: 'center', textAlign: 'center',
letterSpacing: 0.2,
}, },
message: { message: {
fontSize: 16, fontSize: 15,
marginBottom: 20, marginBottom: 24,
textAlign: 'center', textAlign: 'center',
lineHeight: 22,
letterSpacing: 0.1,
}, },
actionsRow: { actionsRow: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'flex-end', justifyContent: 'flex-end',
gap: 12, gap: 12,
marginTop: 4,
}, },
actionButton: { actionButton: {
paddingHorizontal: 16, paddingHorizontal: 20,
paddingVertical: 8, paddingVertical: 11,
borderRadius: 8, borderRadius: 12,
minWidth: 80,
alignItems: 'center',
justifyContent: 'center',
}, },
lastActionButton: { primaryButton: {
// Optionally style the last button differently // Background color set dynamically via theme
},
secondaryButton: {
backgroundColor: 'rgba(255, 255, 255, 0.1)',
}, },
actionText: { actionText: {
fontSize: 16, fontSize: 16,
fontWeight: '600', fontWeight: '600',
letterSpacing: 0.2,
}, },
}); });

View file

@ -38,7 +38,6 @@ import { logger } from '../../utils/logger';
import { TMDBService } from '../../services/tmdbService'; import { TMDBService } from '../../services/tmdbService';
import TrailerService from '../../services/trailerService'; import TrailerService from '../../services/trailerService';
import TrailerPlayer from '../video/TrailerPlayer'; import TrailerPlayer from '../video/TrailerPlayer';
import { isTmdbUrl } from '../../utils/logoUtils';
const { width, height } = Dimensions.get('window'); const { width, height } = Dimensions.get('window');
const isTablet = width >= 768; const isTablet = width >= 768;
@ -895,17 +894,10 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
bannerImage || metadata.banner || metadata.poster bannerImage || metadata.banner || metadata.poster
, [bannerImage, metadata.banner, metadata.poster]); , [bannerImage, metadata.banner, metadata.poster]);
// Prefer TMDB logo when enrichment is enabled; fallback to addon's logo // Use the logo provided by metadata (already enriched by useMetadataAssets based on settings)
const logoUri = useMemo(() => { const logoUri = useMemo(() => {
const candidate = metadata?.logo as string | undefined; return metadata?.logo as string | undefined;
if (!candidate) return undefined; }, [metadata?.logo]);
if (settings?.enrichMetadataWithTMDB) {
// If the current logo is a TMDB URL, use it; otherwise still use available logo
if (isTmdbUrl(candidate)) return candidate;
return candidate;
}
return candidate;
}, [metadata.logo, settings?.enrichMetadataWithTMDB]);
// Performance optimization: Lazy loading setup // Performance optimization: Lazy loading setup
useEffect(() => { useEffect(() => {

View file

@ -26,11 +26,34 @@ import Animated, {
import { NavigationProp } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native';
import { RootStackParamList } from '../navigation/AppNavigator'; import { RootStackParamList } from '../navigation/AppNavigator';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import FastImage from '@d11/react-native-fast-image';
import { useDownloads } from '../contexts/DownloadsContext'; import { useDownloads } from '../contexts/DownloadsContext';
import type { DownloadItem } from '../contexts/DownloadsContext'; import type { DownloadItem } from '../contexts/DownloadsContext';
import { Toast } from 'toastify-react-native'; import { Toast } from 'toastify-react-native';
import CustomAlert from '../components/CustomAlert';
const { height, width } = Dimensions.get('window'); const { height, width } = Dimensions.get('window');
const isTablet = width >= 768;
// Tablet-optimized poster sizes
const HORIZONTAL_ITEM_WIDTH = isTablet ? width * 0.18 : width * 0.3;
const HORIZONTAL_POSTER_HEIGHT = HORIZONTAL_ITEM_WIDTH * 1.5;
const POSTER_WIDTH = isTablet ? 70 : 90;
const POSTER_HEIGHT = isTablet ? 105 : 135;
// Helper function to optimize poster URLs
const optimizePosterUrl = (poster: string | undefined | null): string => {
if (!poster || poster.includes('placeholder')) {
return 'https://via.placeholder.com/80x120/333333/666666?text=No+Image';
}
// For TMDB images, use larger sizes for bigger posters
if (poster.includes('image.tmdb.org')) {
return poster.replace(/\/w\d+\//, '/w300/');
}
return poster;
};
// Download items come from DownloadsContext // Download items come from DownloadsContext
@ -74,6 +97,16 @@ const DownloadItemComponent: React.FC<{
onAction: (item: DownloadItem, action: 'pause' | 'resume' | 'cancel' | 'retry') => void; onAction: (item: DownloadItem, action: 'pause' | 'resume' | 'cancel' | 'retry') => void;
}> = React.memo(({ item, onPress, onAction }) => { }> = React.memo(({ item, onPress, onAction }) => {
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const [posterUrl, setPosterUrl] = useState<string | null>(item.posterUrl || null);
// Try to fetch poster if not available
useEffect(() => {
if (!posterUrl && (item.imdbId || item.tmdbId)) {
// This could be enhanced to fetch poster from TMDB API if needed
// For now, we'll use the existing posterUrl or fallback to placeholder
setPosterUrl(item.posterUrl || null);
}
}, [item.imdbId, item.tmdbId, item.posterUrl, posterUrl]);
const handleLongPress = useCallback(() => { const handleLongPress = useCallback(() => {
if (item.status === 'completed' && item.fileUri) { if (item.status === 'completed' && item.fileUri) {
@ -171,6 +204,29 @@ const DownloadItemComponent: React.FC<{
onLongPress={handleLongPress} onLongPress={handleLongPress}
activeOpacity={0.8} activeOpacity={0.8}
> >
{/* Poster */}
<View style={styles.posterContainer}>
<FastImage
source={{ uri: optimizePosterUrl(posterUrl) }}
style={styles.poster}
resizeMode={FastImage.resizeMode.cover}
/>
{/* Status indicator overlay */}
<View style={[styles.statusOverlay, { backgroundColor: getStatusColor() }]}>
<MaterialCommunityIcons
name={
item.status === 'completed' ? 'check' :
item.status === 'downloading' ? 'download' :
item.status === 'paused' ? 'pause' :
item.status === 'error' ? 'alert-circle' :
'clock'
}
size={12}
color="white"
/>
</View>
</View>
{/* Content info */} {/* Content info */}
<View style={styles.downloadContent}> <View style={styles.downloadContent}>
<View style={styles.downloadHeader}> <View style={styles.downloadHeader}>
@ -209,6 +265,20 @@ const DownloadItemComponent: React.FC<{
</Text> </Text>
</View> </View>
{/* Warning for small files */}
{item.totalBytes && item.totalBytes < 1048576 && ( // Less than 1MB
<View style={styles.warningRow}>
<MaterialCommunityIcons
name="alert-circle"
size={14}
color={currentTheme.colors.warning || '#FF9500'}
/>
<Text style={[styles.warningText, { color: currentTheme.colors.warning || '#FF9500' }]}>
May not play - streaming playlist
</Text>
</View>
)}
{/* Progress bar */} {/* Progress bar */}
<View style={[styles.progressContainer, { backgroundColor: currentTheme.colors.elevation1 }]}> <View style={[styles.progressContainer, { backgroundColor: currentTheme.colors.elevation1 }]}>
<Animated.View <Animated.View
@ -284,6 +354,7 @@ const DownloadsScreen: React.FC = () => {
const [isRefreshing, setIsRefreshing] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false);
const [selectedFilter, setSelectedFilter] = useState<'all' | 'downloading' | 'completed' | 'paused'>('all'); const [selectedFilter, setSelectedFilter] = useState<'all' | 'downloading' | 'completed' | 'paused'>('all');
const [showHelpAlert, setShowHelpAlert] = useState(false);
// Animation values // Animation values
const headerOpacity = useSharedValue(1); const headerOpacity = useSharedValue(1);
@ -383,6 +454,11 @@ const DownloadsScreen: React.FC = () => {
setSelectedFilter(filter); setSelectedFilter(filter);
}, []); }, []);
const showDownloadHelp = useCallback(() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
setShowHelpAlert(true);
}, []);
// Focus effect // Focus effect
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
@ -462,9 +538,22 @@ const DownloadsScreen: React.FC = () => {
}, },
headerStyle, headerStyle,
]}> ]}>
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}> <View style={styles.headerTitleRow}>
Downloads <Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
</Text> Downloads
</Text>
<TouchableOpacity
style={styles.helpButton}
onPress={showDownloadHelp}
activeOpacity={0.7}
>
<MaterialCommunityIcons
name="help-circle-outline"
size={24}
color={currentTheme.colors.mediumEmphasis}
/>
</TouchableOpacity>
</View>
{downloads.length > 0 && ( {downloads.length > 0 && (
<View style={styles.filterContainer}> <View style={styles.filterContainer}>
@ -518,6 +607,14 @@ const DownloadsScreen: React.FC = () => {
)} )}
/> />
)} )}
{/* Help Alert */}
<CustomAlert
visible={showHelpAlert}
title="Download Limitations"
message="• Files smaller than 1MB are typically M3U8 streaming playlists and cannot be downloaded for offline viewing. These only work with online streaming and contain links to video segments, not the actual video content."
onClose={() => setShowHelpAlert(false)}
/>
</View> </View>
); );
}; };
@ -527,29 +624,38 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
}, },
header: { header: {
paddingHorizontal: 20, paddingHorizontal: isTablet ? 24 : 20,
paddingBottom: 16, paddingBottom: isTablet ? 20 : 16,
borderBottomWidth: StyleSheet.hairlineWidth, borderBottomWidth: StyleSheet.hairlineWidth,
}, },
headerTitleRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: isTablet ? 20 : 16,
},
headerTitle: { headerTitle: {
fontSize: 32, fontSize: isTablet ? 36 : 32,
fontWeight: '700', fontWeight: '700',
marginBottom: 16, },
helpButton: {
padding: 8,
marginLeft: 8,
}, },
filterContainer: { filterContainer: {
flexDirection: 'row', flexDirection: 'row',
gap: 12, gap: isTablet ? 16 : 12,
}, },
filterButton: { filterButton: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
paddingHorizontal: 16, paddingHorizontal: isTablet ? 20 : 16,
paddingVertical: 8, paddingVertical: isTablet ? 10 : 8,
borderRadius: 20, borderRadius: 20,
gap: 8, gap: 8,
}, },
filterButtonText: { filterButtonText: {
fontSize: 14, fontSize: isTablet ? 16 : 14,
fontWeight: '600', fontWeight: '600',
}, },
filterBadge: { filterBadge: {
@ -565,24 +671,54 @@ const styles = StyleSheet.create({
fontWeight: '700', fontWeight: '700',
}, },
listContainer: { listContainer: {
padding: 20, padding: isTablet ? 24 : 20,
paddingTop: 8, paddingTop: 8,
paddingBottom: isTablet ? 120 : 100, // Extra padding for tablet bottom nav
}, },
downloadItem: { downloadItem: {
borderRadius: 16, borderRadius: 16,
padding: 16, padding: isTablet ? 20 : 16,
marginBottom: 12, marginBottom: isTablet ? 16 : 12,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
minHeight: isTablet ? 165 : 152, // Accommodate tablet poster + padding
shadowColor: '#000', shadowColor: '#000',
shadowOffset: { width: 0, height: 2 }, shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1, shadowOpacity: 0.1,
shadowRadius: 8, shadowRadius: 8,
elevation: 3, elevation: 3,
}, },
posterContainer: {
width: POSTER_WIDTH,
height: POSTER_HEIGHT,
borderRadius: 8,
marginRight: isTablet ? 20 : 16,
position: 'relative',
overflow: 'hidden',
backgroundColor: '#333',
},
poster: {
width: '100%',
height: '100%',
borderRadius: 8,
},
statusOverlay: {
position: 'absolute',
top: 4,
right: 4,
width: 20,
height: 20,
borderRadius: 10,
alignItems: 'center',
justifyContent: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.3,
shadowRadius: 2,
elevation: 2,
},
downloadContent: { downloadContent: {
flex: 1, flex: 1,
marginRight: 12,
}, },
downloadHeader: { downloadHeader: {
marginBottom: 12, marginBottom: 12,
@ -627,6 +763,16 @@ const styles = StyleSheet.create({
sizeRow: { sizeRow: {
marginBottom: 6, marginBottom: 6,
}, },
warningRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
marginBottom: 6,
},
warningText: {
fontSize: 11,
fontWeight: '500',
},
progressInfo: { progressInfo: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
@ -679,7 +825,8 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
paddingHorizontal: 40, paddingHorizontal: isTablet ? 64 : 40,
paddingBottom: isTablet ? 120 : 100,
}, },
emptyIconContainer: { emptyIconContainer: {
width: 96, width: 96,
@ -690,16 +837,16 @@ const styles = StyleSheet.create({
marginBottom: 24, marginBottom: 24,
}, },
emptyTitle: { emptyTitle: {
fontSize: 24, fontSize: isTablet ? 28 : 24,
fontWeight: '700', fontWeight: '700',
marginBottom: 8, marginBottom: 8,
textAlign: 'center', textAlign: 'center',
}, },
emptySubtitle: { emptySubtitle: {
fontSize: 16, fontSize: isTablet ? 18 : 16,
textAlign: 'center', textAlign: 'center',
lineHeight: 24, lineHeight: isTablet ? 28 : 24,
marginBottom: 32, marginBottom: isTablet ? 40 : 32,
}, },
exploreButton: { exploreButton: {
paddingHorizontal: 24, paddingHorizontal: 24,
@ -713,7 +860,7 @@ const styles = StyleSheet.create({
emptyFilterContainer: { emptyFilterContainer: {
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
paddingVertical: 60, paddingVertical: isTablet ? 80 : 60,
}, },
emptyFilterTitle: { emptyFilterTitle: {
fontSize: 18, fontSize: 18,

View file

@ -282,8 +282,6 @@ const SettingsScreen: React.FC = () => {
// Tablet-specific state // Tablet-specific state
const [selectedCategory, setSelectedCategory] = useState('account'); const [selectedCategory, setSelectedCategory] = useState('account');
const [downloadsDevUnlocked, setDownloadsDevUnlocked] = useState(false);
const [versionTapCount, setVersionTapCount] = useState(0);
// States for dynamic content // States for dynamic content
const [addonCount, setAddonCount] = useState<number>(0); const [addonCount, setAddonCount] = useState<number>(0);
@ -582,22 +580,20 @@ const SettingsScreen: React.FC = () => {
)} )}
isTablet={isTablet} isTablet={isTablet}
/> />
{downloadsDevUnlocked && ( <SettingItem
<SettingItem title="Enable Downloads"
title="Enable Downloads" description="Show Downloads tab and enable saving streams"
description="Show Downloads tab and enable saving streams" icon="download"
icon="download" renderControl={() => (
renderControl={() => ( <Switch
<Switch value={settings?.enableDownloads ?? false}
value={settings?.enableDownloads ?? true} onValueChange={(value) => updateSetting('enableDownloads', value)}
onValueChange={(value) => updateSetting('enableDownloads', value)} trackColor={{ false: 'rgba(255,255,255,0.2)', true: currentTheme.colors.primary }}
trackColor={{ false: 'rgba(255,255,255,0.2)', true: currentTheme.colors.primary }} thumbColor={settings?.enableDownloads ? '#fff' : '#f4f3f4'}
thumbColor={settings?.enableDownloads ? '#fff' : '#f4f3f4'} />
/> )}
)} isTablet={isTablet}
isTablet={isTablet} />
/>
)}
<SettingItem <SettingItem
title="Notifications" title="Notifications"
description="Episode reminders" description="Episode reminders"
@ -631,16 +627,6 @@ const SettingsScreen: React.FC = () => {
title="Version" title="Version"
description={getDisplayedAppVersion()} description={getDisplayedAppVersion()}
icon="info-outline" icon="info-outline"
onPress={() => {
if (downloadsDevUnlocked) return;
const next = versionTapCount + 1;
setVersionTapCount(next);
if (next >= 5) {
setDownloadsDevUnlocked(true);
setVersionTapCount(0);
openAlert('Developer option unlocked', 'Downloads toggle is now visible in Playback settings.');
}
}}
isLast={true} isLast={true}
isTablet={isTablet} isTablet={isTablet}
/> />

View file

@ -238,18 +238,14 @@ const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, the
Toast.success('Stream URL copied to clipboard!', 'bottom'); Toast.success('Stream URL copied to clipboard!', 'bottom');
} else { } else {
// iOS uses custom alert // iOS uses custom alert
setTimeout(() => { showAlert('Copied!', 'Stream URL has been copied to clipboard.');
showAlert('Copied!', 'Stream URL has been copied to clipboard.');
}, 50);
} }
} catch (error) { } catch (error) {
// Fallback: show URL in alert if clipboard fails // Fallback: show URL in alert if clipboard fails
if (Platform.OS === 'android') { if (Platform.OS === 'android') {
Toast.info(`Stream URL: ${stream.url}`, 'bottom'); Toast.info(`Stream URL: ${stream.url}`, 'bottom');
} else { } else {
setTimeout(() => { showAlert('Stream URL', stream.url);
showAlert('Stream URL', stream.url);
}, 50);
} }
} }
} }
@ -331,7 +327,7 @@ const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, the
imdbId: parentImdbId || parent.imdbId || undefined, imdbId: parentImdbId || parent.imdbId || undefined,
tmdbId: tmdbId, tmdbId: tmdbId,
}); });
Toast.success('Download started', 'bottom'); showAlert('Download Started', 'Your download has been added to the queue.');
} catch {} } catch {}
}, [startDownload, stream.url, stream.headers, streamInfo.quality, showAlert, stream.name, stream.title, parentId, parentImdbId, parentTitle, parentType, parentSeason, parentEpisode, parentEpisodeTitle, parentPosterUrl, providerName]); }, [startDownload, stream.url, stream.headers, streamInfo.quality, showAlert, stream.name, stream.title, parentId, parentImdbId, parentTitle, parentType, parentSeason, parentEpisode, parentEpisodeTitle, parentPosterUrl, providerName]);
@ -343,7 +339,6 @@ const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, the
isLoading && styles.streamCardLoading, isLoading && styles.streamCardLoading,
isDebrid && styles.streamCardHighlighted isDebrid && styles.streamCardHighlighted
]} ]}
onPress={onPress}
onLongPress={handleLongPress} onLongPress={handleLongPress}
disabled={isLoading} disabled={isLoading}
activeOpacity={0.7} activeOpacity={0.7}
@ -402,13 +397,17 @@ const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, the
</View> </View>
</View> </View>
<View style={styles.streamAction}> <TouchableOpacity
style={styles.streamAction}
onPress={() => onPress()}
activeOpacity={0.7}
>
<MaterialIcons <MaterialIcons
name="play-arrow" name="play-arrow"
size={22} size={22}
color={theme.colors.white} color={theme.colors.white}
/> />
</View> </TouchableOpacity>
{settings?.enableDownloads !== false && ( {settings?.enableDownloads !== false && (
<TouchableOpacity <TouchableOpacity
style={[styles.streamAction, { marginLeft: 8, backgroundColor: theme.colors.elevation2 }]} style={[styles.streamAction, { marginLeft: 8, backgroundColor: theme.colors.elevation2 }]}
@ -2204,7 +2203,7 @@ export const StreamsScreen = () => {
parentPosterUrl={episodeImage || metadata?.poster || undefined} parentPosterUrl={episodeImage || metadata?.poster || undefined}
providerName={streams && Object.keys(streams).find(pid => (streams as any)[pid]?.streams?.includes?.(item))} providerName={streams && Object.keys(streams).find(pid => (streams as any)[pid]?.streams?.includes?.(item))}
parentId={id} parentId={id}
parentImdbId={imdbId} parentImdbId={imdbId || undefined}
/> />
</View> </View>
)} )}

View file

@ -49,8 +49,8 @@ export const colors = {
surfaceVariant: 'rgba(255, 255, 255, 0.03)', // Material dark theme surface variant surfaceVariant: 'rgba(255, 255, 255, 0.03)', // Material dark theme surface variant
// Material Design elevation overlays // Material Design elevation overlays
elevation1: 'rgba(255, 255, 255, 0.05)', elevation1: 'rgba(255, 255, 255, 0.03)',
elevation2: 'rgba(255, 255, 255, 0.08)', elevation2: 'rgba(255, 255, 255, 0.03)',
elevation3: 'rgba(255, 255, 255, 0.11)', elevation3: 'rgba(255, 255, 255, 0.11)',
elevation4: 'rgba(255, 255, 255, 0.12)', elevation4: 'rgba(255, 255, 255, 0.12)',