mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
UI changes
This commit is contained in:
parent
9e7b9c5fe4
commit
1a9d59e804
6 changed files with 333 additions and 136 deletions
|
|
@ -44,13 +44,13 @@ export const CustomAlert = ({
|
|||
const themeColors = currentTheme.colors;
|
||||
|
||||
useEffect(() => {
|
||||
const animDuration = Platform.OS === 'android' ? 200 : 120;
|
||||
const duration = Platform.OS === 'android' ? 200 : 150;
|
||||
if (visible) {
|
||||
opacity.value = withTiming(1, { duration: animDuration });
|
||||
scale.value = withTiming(1, { duration: animDuration });
|
||||
opacity.value = withTiming(1, { duration });
|
||||
scale.value = withTiming(1, { duration });
|
||||
} else {
|
||||
opacity.value = withTiming(0, { duration: animDuration });
|
||||
scale.value = withTiming(0.95, { duration: animDuration });
|
||||
opacity.value = withTiming(0, { duration });
|
||||
scale.value = withTiming(0.95, { duration });
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
|
|
@ -63,10 +63,6 @@ export const CustomAlert = ({
|
|||
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
|
||||
const handleActionPress = useCallback((action: { label: string; onPress: () => void; style?: object }) => {
|
||||
try {
|
||||
|
|
@ -95,23 +91,54 @@ export const CustomAlert = ({
|
|||
statusBarTranslucent={false}
|
||||
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} />
|
||||
<View style={styles.centered}>
|
||||
<View style={[styles.alertContainer, { backgroundColor, borderColor }]}>
|
||||
<Text style={[styles.title, { color: textColor }]}>{title}</Text>
|
||||
<Text style={[styles.message, { color: textColor }]}>{message}</Text>
|
||||
<View style={[
|
||||
styles.alertContainer,
|
||||
{
|
||||
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}>
|
||||
{actions.map((action, idx) => (
|
||||
<TouchableOpacity
|
||||
key={action.label}
|
||||
style={[styles.actionButton, idx === actions.length - 1 && styles.lastActionButton, action.style]}
|
||||
onPress={() => handleActionPress(action)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={[styles.actionText, { color: themeColors.primary }]}>{action.label}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
{actions.map((action, idx) => {
|
||||
const isPrimary = idx === actions.length - 1;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={action.label}
|
||||
style={[
|
||||
styles.actionButton,
|
||||
isPrimary
|
||||
? { ...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>
|
||||
|
|
@ -129,23 +156,55 @@ export const CustomAlert = ({
|
|||
onRequestClose={onClose}
|
||||
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} />
|
||||
<View style={styles.centered}>
|
||||
<Animated.View style={[styles.alertContainer, alertStyle, { backgroundColor, borderColor }]}>
|
||||
<Text style={[styles.title, { color: textColor }]}>{title}</Text>
|
||||
<Text style={[styles.message, { color: textColor }]}>{message}</Text>
|
||||
<Animated.View style={[
|
||||
styles.alertContainer,
|
||||
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}>
|
||||
{actions.map((action, idx) => (
|
||||
<TouchableOpacity
|
||||
key={action.label}
|
||||
style={[styles.actionButton, idx === actions.length - 1 && styles.lastActionButton, action.style]}
|
||||
onPress={() => handleActionPress(action)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={[styles.actionText, { color: themeColors.primary }]}>{action.label}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
{actions.map((action, idx) => {
|
||||
const isPrimary = idx === actions.length - 1;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={action.label}
|
||||
style={[
|
||||
styles.actionButton,
|
||||
isPrimary
|
||||
? { ...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>
|
||||
</Animated.View>
|
||||
</View>
|
||||
|
|
@ -167,52 +226,66 @@ const styles = StyleSheet.create({
|
|||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 24,
|
||||
},
|
||||
alertContainer: {
|
||||
minWidth: 280,
|
||||
maxWidth: '85%',
|
||||
borderRadius: 20,
|
||||
padding: 24,
|
||||
width: '100%',
|
||||
maxWidth: 340,
|
||||
borderRadius: 24,
|
||||
padding: 28,
|
||||
borderWidth: 1,
|
||||
borderColor: '#007AFF', // iOS blue - will be overridden by theme
|
||||
overflow: 'hidden', // Ensure background fills entire card
|
||||
...Platform.select({
|
||||
ios: {
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 8,
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 24,
|
||||
},
|
||||
android: {
|
||||
elevation: 8,
|
||||
elevation: 12,
|
||||
},
|
||||
}),
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
marginBottom: 12,
|
||||
marginBottom: 8,
|
||||
textAlign: 'center',
|
||||
letterSpacing: 0.2,
|
||||
},
|
||||
message: {
|
||||
fontSize: 16,
|
||||
marginBottom: 20,
|
||||
fontSize: 15,
|
||||
marginBottom: 24,
|
||||
textAlign: 'center',
|
||||
lineHeight: 22,
|
||||
letterSpacing: 0.1,
|
||||
},
|
||||
actionsRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
gap: 12,
|
||||
marginTop: 4,
|
||||
},
|
||||
actionButton: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 11,
|
||||
borderRadius: 12,
|
||||
minWidth: 80,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
lastActionButton: {
|
||||
// Optionally style the last button differently
|
||||
primaryButton: {
|
||||
// Background color set dynamically via theme
|
||||
},
|
||||
secondaryButton: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
},
|
||||
actionText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
letterSpacing: 0.2,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ import { logger } from '../../utils/logger';
|
|||
import { TMDBService } from '../../services/tmdbService';
|
||||
import TrailerService from '../../services/trailerService';
|
||||
import TrailerPlayer from '../video/TrailerPlayer';
|
||||
import { isTmdbUrl } from '../../utils/logoUtils';
|
||||
|
||||
const { width, height } = Dimensions.get('window');
|
||||
const isTablet = width >= 768;
|
||||
|
|
@ -895,17 +894,10 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
|||
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 candidate = metadata?.logo as string | undefined;
|
||||
if (!candidate) return undefined;
|
||||
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]);
|
||||
return metadata?.logo as string | undefined;
|
||||
}, [metadata?.logo]);
|
||||
|
||||
// Performance optimization: Lazy loading setup
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -26,11 +26,34 @@ import Animated, {
|
|||
import { NavigationProp } from '@react-navigation/native';
|
||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import FastImage from '@d11/react-native-fast-image';
|
||||
import { useDownloads } from '../contexts/DownloadsContext';
|
||||
import type { DownloadItem } from '../contexts/DownloadsContext';
|
||||
import { Toast } from 'toastify-react-native';
|
||||
import CustomAlert from '../components/CustomAlert';
|
||||
|
||||
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
|
||||
|
||||
|
|
@ -74,6 +97,16 @@ const DownloadItemComponent: React.FC<{
|
|||
onAction: (item: DownloadItem, action: 'pause' | 'resume' | 'cancel' | 'retry') => void;
|
||||
}> = React.memo(({ item, onPress, onAction }) => {
|
||||
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(() => {
|
||||
if (item.status === 'completed' && item.fileUri) {
|
||||
|
|
@ -171,6 +204,29 @@ const DownloadItemComponent: React.FC<{
|
|||
onLongPress={handleLongPress}
|
||||
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 */}
|
||||
<View style={styles.downloadContent}>
|
||||
<View style={styles.downloadHeader}>
|
||||
|
|
@ -208,7 +264,21 @@ const DownloadItemComponent: React.FC<{
|
|||
{formatBytes(item.downloadedBytes)} / {item.totalBytes ? formatBytes(item.totalBytes) : '—'}
|
||||
</Text>
|
||||
</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 */}
|
||||
<View style={[styles.progressContainer, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||
<Animated.View
|
||||
|
|
@ -284,6 +354,7 @@ const DownloadsScreen: React.FC = () => {
|
|||
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [selectedFilter, setSelectedFilter] = useState<'all' | 'downloading' | 'completed' | 'paused'>('all');
|
||||
const [showHelpAlert, setShowHelpAlert] = useState(false);
|
||||
|
||||
// Animation values
|
||||
const headerOpacity = useSharedValue(1);
|
||||
|
|
@ -383,6 +454,11 @@ const DownloadsScreen: React.FC = () => {
|
|||
setSelectedFilter(filter);
|
||||
}, []);
|
||||
|
||||
const showDownloadHelp = useCallback(() => {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
setShowHelpAlert(true);
|
||||
}, []);
|
||||
|
||||
// Focus effect
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
|
|
@ -462,9 +538,22 @@ const DownloadsScreen: React.FC = () => {
|
|||
},
|
||||
headerStyle,
|
||||
]}>
|
||||
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
|
||||
Downloads
|
||||
</Text>
|
||||
<View style={styles.headerTitleRow}>
|
||||
<Text style={[styles.headerTitle, { color: currentTheme.colors.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 && (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
|
@ -527,29 +624,38 @@ const styles = StyleSheet.create({
|
|||
flex: 1,
|
||||
},
|
||||
header: {
|
||||
paddingHorizontal: 20,
|
||||
paddingBottom: 16,
|
||||
paddingHorizontal: isTablet ? 24 : 20,
|
||||
paddingBottom: isTablet ? 20 : 16,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
headerTitleRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: isTablet ? 20 : 16,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 32,
|
||||
fontSize: isTablet ? 36 : 32,
|
||||
fontWeight: '700',
|
||||
marginBottom: 16,
|
||||
},
|
||||
helpButton: {
|
||||
padding: 8,
|
||||
marginLeft: 8,
|
||||
},
|
||||
filterContainer: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
gap: isTablet ? 16 : 12,
|
||||
},
|
||||
filterButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: isTablet ? 20 : 16,
|
||||
paddingVertical: isTablet ? 10 : 8,
|
||||
borderRadius: 20,
|
||||
gap: 8,
|
||||
},
|
||||
filterButtonText: {
|
||||
fontSize: 14,
|
||||
fontSize: isTablet ? 16 : 14,
|
||||
fontWeight: '600',
|
||||
},
|
||||
filterBadge: {
|
||||
|
|
@ -565,24 +671,54 @@ const styles = StyleSheet.create({
|
|||
fontWeight: '700',
|
||||
},
|
||||
listContainer: {
|
||||
padding: 20,
|
||||
padding: isTablet ? 24 : 20,
|
||||
paddingTop: 8,
|
||||
paddingBottom: isTablet ? 120 : 100, // Extra padding for tablet bottom nav
|
||||
},
|
||||
downloadItem: {
|
||||
borderRadius: 16,
|
||||
padding: 16,
|
||||
marginBottom: 12,
|
||||
padding: isTablet ? 20 : 16,
|
||||
marginBottom: isTablet ? 16 : 12,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
minHeight: isTablet ? 165 : 152, // Accommodate tablet poster + padding
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 8,
|
||||
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: {
|
||||
flex: 1,
|
||||
marginRight: 12,
|
||||
},
|
||||
downloadHeader: {
|
||||
marginBottom: 12,
|
||||
|
|
@ -627,6 +763,16 @@ const styles = StyleSheet.create({
|
|||
sizeRow: {
|
||||
marginBottom: 6,
|
||||
},
|
||||
warningRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
marginBottom: 6,
|
||||
},
|
||||
warningText: {
|
||||
fontSize: 11,
|
||||
fontWeight: '500',
|
||||
},
|
||||
progressInfo: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
|
|
@ -679,7 +825,8 @@ const styles = StyleSheet.create({
|
|||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 40,
|
||||
paddingHorizontal: isTablet ? 64 : 40,
|
||||
paddingBottom: isTablet ? 120 : 100,
|
||||
},
|
||||
emptyIconContainer: {
|
||||
width: 96,
|
||||
|
|
@ -690,16 +837,16 @@ const styles = StyleSheet.create({
|
|||
marginBottom: 24,
|
||||
},
|
||||
emptyTitle: {
|
||||
fontSize: 24,
|
||||
fontSize: isTablet ? 28 : 24,
|
||||
fontWeight: '700',
|
||||
marginBottom: 8,
|
||||
textAlign: 'center',
|
||||
},
|
||||
emptySubtitle: {
|
||||
fontSize: 16,
|
||||
fontSize: isTablet ? 18 : 16,
|
||||
textAlign: 'center',
|
||||
lineHeight: 24,
|
||||
marginBottom: 32,
|
||||
lineHeight: isTablet ? 28 : 24,
|
||||
marginBottom: isTablet ? 40 : 32,
|
||||
},
|
||||
exploreButton: {
|
||||
paddingHorizontal: 24,
|
||||
|
|
@ -713,7 +860,7 @@ const styles = StyleSheet.create({
|
|||
emptyFilterContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 60,
|
||||
paddingVertical: isTablet ? 80 : 60,
|
||||
},
|
||||
emptyFilterTitle: {
|
||||
fontSize: 18,
|
||||
|
|
|
|||
|
|
@ -282,8 +282,6 @@ const SettingsScreen: React.FC = () => {
|
|||
|
||||
// Tablet-specific state
|
||||
const [selectedCategory, setSelectedCategory] = useState('account');
|
||||
const [downloadsDevUnlocked, setDownloadsDevUnlocked] = useState(false);
|
||||
const [versionTapCount, setVersionTapCount] = useState(0);
|
||||
|
||||
// States for dynamic content
|
||||
const [addonCount, setAddonCount] = useState<number>(0);
|
||||
|
|
@ -582,22 +580,20 @@ const SettingsScreen: React.FC = () => {
|
|||
)}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
{downloadsDevUnlocked && (
|
||||
<SettingItem
|
||||
title="Enable Downloads"
|
||||
description="Show Downloads tab and enable saving streams"
|
||||
icon="download"
|
||||
renderControl={() => (
|
||||
<Switch
|
||||
value={settings?.enableDownloads ?? true}
|
||||
onValueChange={(value) => updateSetting('enableDownloads', value)}
|
||||
trackColor={{ false: 'rgba(255,255,255,0.2)', true: currentTheme.colors.primary }}
|
||||
thumbColor={settings?.enableDownloads ? '#fff' : '#f4f3f4'}
|
||||
/>
|
||||
)}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
)}
|
||||
<SettingItem
|
||||
title="Enable Downloads"
|
||||
description="Show Downloads tab and enable saving streams"
|
||||
icon="download"
|
||||
renderControl={() => (
|
||||
<Switch
|
||||
value={settings?.enableDownloads ?? false}
|
||||
onValueChange={(value) => updateSetting('enableDownloads', value)}
|
||||
trackColor={{ false: 'rgba(255,255,255,0.2)', true: currentTheme.colors.primary }}
|
||||
thumbColor={settings?.enableDownloads ? '#fff' : '#f4f3f4'}
|
||||
/>
|
||||
)}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
<SettingItem
|
||||
title="Notifications"
|
||||
description="Episode reminders"
|
||||
|
|
@ -631,16 +627,6 @@ const SettingsScreen: React.FC = () => {
|
|||
title="Version"
|
||||
description={getDisplayedAppVersion()}
|
||||
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}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -238,18 +238,14 @@ const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, the
|
|||
Toast.success('Stream URL copied to clipboard!', 'bottom');
|
||||
} else {
|
||||
// iOS uses custom alert
|
||||
setTimeout(() => {
|
||||
showAlert('Copied!', 'Stream URL has been copied to clipboard.');
|
||||
}, 50);
|
||||
showAlert('Copied!', 'Stream URL has been copied to clipboard.');
|
||||
}
|
||||
} catch (error) {
|
||||
// Fallback: show URL in alert if clipboard fails
|
||||
if (Platform.OS === 'android') {
|
||||
Toast.info(`Stream URL: ${stream.url}`, 'bottom');
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
showAlert('Stream URL', stream.url);
|
||||
}, 50);
|
||||
showAlert('Stream URL', stream.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -331,19 +327,18 @@ const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, the
|
|||
imdbId: parentImdbId || parent.imdbId || undefined,
|
||||
tmdbId: tmdbId,
|
||||
});
|
||||
Toast.success('Download started', 'bottom');
|
||||
showAlert('Download Started', 'Your download has been added to the queue.');
|
||||
} catch {}
|
||||
}, [startDownload, stream.url, stream.headers, streamInfo.quality, showAlert, stream.name, stream.title, parentId, parentImdbId, parentTitle, parentType, parentSeason, parentEpisode, parentEpisodeTitle, parentPosterUrl, providerName]);
|
||||
|
||||
const isDebrid = streamInfo.isDebrid;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.streamCard,
|
||||
styles.streamCard,
|
||||
isLoading && styles.streamCardLoading,
|
||||
isDebrid && styles.streamCardHighlighted
|
||||
]}
|
||||
onPress={onPress}
|
||||
]}
|
||||
onLongPress={handleLongPress}
|
||||
disabled={isLoading}
|
||||
activeOpacity={0.7}
|
||||
|
|
@ -402,20 +397,24 @@ const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, the
|
|||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.streamAction}>
|
||||
<MaterialIcons
|
||||
name="play-arrow"
|
||||
size={22}
|
||||
color={theme.colors.white}
|
||||
<TouchableOpacity
|
||||
style={styles.streamAction}
|
||||
onPress={() => onPress()}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="play-arrow"
|
||||
size={22}
|
||||
color={theme.colors.white}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
{settings?.enableDownloads !== false && (
|
||||
<TouchableOpacity
|
||||
style={[styles.streamAction, { marginLeft: 8, backgroundColor: theme.colors.elevation2 }]}
|
||||
onPress={handleDownload}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<MaterialIcons
|
||||
<MaterialIcons
|
||||
name="download"
|
||||
size={20}
|
||||
color={theme.colors.highEmphasis}
|
||||
|
|
@ -2204,7 +2203,7 @@ export const StreamsScreen = () => {
|
|||
parentPosterUrl={episodeImage || metadata?.poster || undefined}
|
||||
providerName={streams && Object.keys(streams).find(pid => (streams as any)[pid]?.streams?.includes?.(item))}
|
||||
parentId={id}
|
||||
parentImdbId={imdbId}
|
||||
parentImdbId={imdbId || undefined}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -49,8 +49,8 @@ export const colors = {
|
|||
surfaceVariant: 'rgba(255, 255, 255, 0.03)', // Material dark theme surface variant
|
||||
|
||||
// Material Design elevation overlays
|
||||
elevation1: 'rgba(255, 255, 255, 0.05)',
|
||||
elevation2: 'rgba(255, 255, 255, 0.08)',
|
||||
elevation1: 'rgba(255, 255, 255, 0.03)',
|
||||
elevation2: 'rgba(255, 255, 255, 0.03)',
|
||||
elevation3: 'rgba(255, 255, 255, 0.11)',
|
||||
elevation4: 'rgba(255, 255, 255, 0.12)',
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue