some UI changes for ContinueWatching

This commit is contained in:
tapframe 2025-09-04 18:00:37 +05:30
parent afaca6467f
commit 9c533e3c52
8 changed files with 77 additions and 29 deletions

View file

@ -2,7 +2,7 @@
"expo": {
"name": "Nuvio",
"slug": "nuvio",
"version": "0.6.0-beta.7",
"version": "0.6.0-beta.8",
"orientation": "default",
"icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png",
"userInterfaceStyle": "dark",
@ -16,7 +16,7 @@
"ios": {
"supportsTablet": true,
"icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png",
"buildNumber": "7",
"buildNumber": "8",
"infoPlist": {
"NSAppTransportSecurity": {
"NSAllowsArbitraryLoads": true
@ -46,7 +46,7 @@
"WAKE_LOCK"
],
"package": "com.nuvio.app",
"versionCode": 7,
"versionCode": 8,
"architectures": [
"arm64-v8a",
"armeabi-v7a",

View file

@ -23,9 +23,10 @@ import { parseISO, isThisWeek, format, isAfter, isBefore } from 'date-fns';
import Animated, { FadeIn } from 'react-native-reanimated';
import { useCalendarData } from '../../hooks/useCalendarData';
// Compute base sizes; actual tablet sizes will be adjusted inside component for responsiveness
const { width } = Dimensions.get('window');
const ITEM_WIDTH = width * 0.75; // Reduced width for better spacing
const ITEM_HEIGHT = 180; // Compact height for cleaner design
const ITEM_WIDTH = width * 0.75; // phone default
const ITEM_HEIGHT = 180; // phone default
interface ThisWeekEpisode {
id: string;
@ -57,6 +58,12 @@ export const ThisWeekSection = React.memo(() => {
const { currentTheme } = useTheme();
const { calendarData, loading } = useCalendarData();
// Responsive sizing for tablets
const deviceWidth = Dimensions.get('window').width;
const isTablet = deviceWidth >= 768;
const computedItemWidth = useMemo(() => (isTablet ? Math.min(deviceWidth * 0.46, 560) : ITEM_WIDTH), [isTablet, deviceWidth]);
const computedItemHeight = useMemo(() => (isTablet ? 220 : ITEM_HEIGHT), [isTablet]);
const thisWeekEpisodes = useMemo(() => {
const thisWeekSection = calendarData.find(section => section.title === 'This Week');
if (!thisWeekSection) return [];
@ -109,7 +116,7 @@ export const ThisWeekSection = React.memo(() => {
item.poster);
return (
<View style={styles.episodeItemContainer}>
<View style={[styles.episodeItemContainer, { width: computedItemWidth, height: computedItemHeight }]}>
<TouchableOpacity
style={[
styles.episodeItem,
@ -143,30 +150,30 @@ export const ThisWeekSection = React.memo(() => {
>
{/* Content area */}
<View style={styles.contentArea}>
<Text style={[styles.seriesName, { color: currentTheme.colors.white }]} numberOfLines={1}>
<Text style={[styles.seriesName, { color: currentTheme.colors.white, fontSize: isTablet ? 18 : undefined }]} numberOfLines={1}>
{item.seriesName}
</Text>
<Text style={[styles.episodeTitle, { color: 'rgba(255,255,255,0.9)' }]} numberOfLines={2}>
<Text style={[styles.episodeTitle, { color: 'rgba(255,255,255,0.9)', fontSize: isTablet ? 16 : undefined }]} numberOfLines={2}>
{item.title}
</Text>
{item.overview && (
<Text style={[styles.overview, { color: 'rgba(255,255,255,0.8)' }]} numberOfLines={2}>
<Text style={[styles.overview, { color: 'rgba(255,255,255,0.8)', fontSize: isTablet ? 13 : undefined }]} numberOfLines={isTablet ? 3 : 2}>
{item.overview}
</Text>
)}
<View style={styles.dateContainer}>
<Text style={[styles.episodeInfo, { color: 'rgba(255,255,255,0.7)' }]}>
<Text style={[styles.episodeInfo, { color: 'rgba(255,255,255,0.7)', fontSize: isTablet ? 13 : undefined }]}>
S{item.season}:E{item.episode}
</Text>
<MaterialIcons
name="event"
size={14}
size={isTablet ? 16 : 14}
color={currentTheme.colors.primary}
/>
<Text style={[styles.releaseDate, { color: currentTheme.colors.primary }]}>
<Text style={[styles.releaseDate, { color: currentTheme.colors.primary, fontSize: isTablet ? 14 : undefined }]}>
{formattedDate}
</Text>
</View>
@ -197,10 +204,19 @@ export const ThisWeekSection = React.memo(() => {
renderItem={renderEpisodeItem}
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.listContent}
snapToInterval={ITEM_WIDTH + 16}
contentContainerStyle={[styles.listContent, { paddingLeft: isTablet ? 24 : 16, paddingRight: isTablet ? 24 : 16 }]}
snapToInterval={computedItemWidth + 16}
decelerationRate="fast"
snapToAlignment="start"
initialNumToRender={isTablet ? 4 : 3}
windowSize={3}
maxToRenderPerBatch={3}
removeClippedSubviews
getItemLayout={(data, index) => {
const length = computedItemWidth + 16;
const offset = length * index;
return { length, offset, index };
}}
ItemSeparatorComponent={() => <View style={{ width: 16 }} />}
/>
</Animated.View>

View file

@ -799,11 +799,15 @@ const AndroidVideoPlayer: React.FC = () => {
// Navigate immediately without delay
ScreenOrientation.unlockAsync().then(() => {
// On iOS, explicitly return to portrait to avoid sticking in landscape
if (Platform.OS === 'ios') {
// On tablets keep rotation unlocked; on phones, return to portrait
const { width: dw, height: dh } = Dimensions.get('window');
const isTablet = Math.min(dw, dh) >= 768 || ((Platform as any).isPad === true);
if (!isTablet) {
setTimeout(() => {
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => {});
}, 50);
} else {
ScreenOrientation.unlockAsync().catch(() => {});
}
disableImmersiveMode();
@ -815,11 +819,15 @@ const AndroidVideoPlayer: React.FC = () => {
(navigation as any).navigate('Streams', { id, type, episodeId, fromPlayer: true });
}
}).catch(() => {
// Fallback: still try to restore portrait then navigate
if (Platform.OS === 'ios') {
// Fallback: still try to restore portrait on phones then navigate
const { width: dw, height: dh } = Dimensions.get('window');
const isTablet = Math.min(dw, dh) >= 768 || ((Platform as any).isPad === true);
if (!isTablet) {
setTimeout(() => {
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => {});
}, 50);
} else {
ScreenOrientation.unlockAsync().catch(() => {});
}
disableImmersiveMode();

View file

@ -838,10 +838,16 @@ const VideoPlayer: React.FC = () => {
logger.warn('[VideoPlayer] Failed to unlock orientation:', orientationError);
}
// On iOS, explicitly return to portrait to avoid sticking in landscape
// On iOS tablets, keep rotation unlocked; on phones, return to portrait
if (Platform.OS === 'ios') {
const { width: dw, height: dh } = Dimensions.get('window');
const isTablet = (Platform as any).isPad === true || Math.min(dw, dh) >= 768;
setTimeout(() => {
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => {});
if (isTablet) {
ScreenOrientation.unlockAsync().catch(() => {});
} else {
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => {});
}
}, 50);
}

View file

@ -100,7 +100,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
// Quality filtering defaults
excludedQualities: [], // No qualities excluded by default
// Playback behavior defaults
alwaysResume: false,
alwaysResume: true,
// Theme defaults
themeId: 'default',
customThemes: [],

View file

@ -369,9 +369,16 @@ const HomeScreen = () => {
StatusBar.setTranslucent(true);
StatusBar.setBackgroundColor('transparent');
// Ensure portrait when coming back to Home on all platforms
// Allow free rotation on tablets; lock portrait on phones
try {
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP);
const { width: dw, height: dh } = Dimensions.get('window');
const smallestDimension = Math.min(dw, dh);
const isTablet = (Platform.OS === 'ios' ? (Platform as any).isPad === true : smallestDimension >= 768);
if (isTablet) {
ScreenOrientation.unlockAsync();
} else {
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP);
}
} catch {}
// For iOS specifically
@ -569,7 +576,6 @@ const HomeScreen = () => {
// Normal flow when addons are present (featured moved to ListHeaderComponent)
data.push({ type: 'thisWeek', key: 'thisWeek' });
data.push({ type: 'continueWatching', key: 'continueWatching' });
// Only show a limited number of catalogs initially for performance
const catalogsToShow = catalogs.slice(0, visibleCatalogCount);
@ -631,6 +637,12 @@ const HomeScreen = () => {
const memoizedThisWeekSection = useMemo(() => <ThisWeekSection />, []);
const memoizedContinueWatchingSection = useMemo(() => <ContinueWatchingSection ref={continueWatchingRef} />, []);
const memoizedHeader = useMemo(() => (
<>
{showHeroSection ? memoizedFeaturedContent : null}
{memoizedContinueWatchingSection}
</>
), [showHeroSection, memoizedFeaturedContent, memoizedContinueWatchingSection]);
// Track scroll direction manually for reliable behavior across platforms
const lastScrollYRef = useRef(0);
const lastToggleRef = useRef(0);
@ -652,7 +664,7 @@ const HomeScreen = () => {
case 'thisWeek':
return wrapper(memoizedThisWeekSection);
case 'continueWatching':
return wrapper(memoizedContinueWatchingSection);
return null; // Moved to ListHeaderComponent to avoid remounts on scroll
case 'catalog':
return wrapper(<CatalogSection catalog={item.catalog} />);
case 'placeholder':
@ -749,7 +761,7 @@ const HomeScreen = () => {
{ paddingTop: insets.top }
]}
showsVerticalScrollIndicator={false}
ListHeaderComponent={showHeroSection ? memoizedFeaturedContent : null}
ListHeaderComponent={memoizedHeader}
ListFooterComponent={ListFooterComponent}
onMomentumScrollEnd={handleScrollEnd}
onEndReached={handleLoadMoreCatalogs}

View file

@ -582,7 +582,7 @@ const SettingsScreen: React.FC = () => {
/>
<SettingItem
title="Version"
description="0.6.0-beta.7"
description="0.6.0-beta.8"
icon="info-outline"
isLast={true}
isTablet={isTablet}

View file

@ -940,9 +940,15 @@ export const StreamsScreen = () => {
logger.warn('[StreamsScreen] MKV support detection failed:', e);
}
// Add pre-navigation orientation lock to reduce glitch
// Add pre-navigation orientation lock to reduce glitch on phones only; tablets can rotate freely
try {
await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE);
const { width: dw, height: dh } = Dimensions.get('window');
const isTablet = Math.min(dw, dh) >= 768 || ((Platform as any).isPad === true);
if (!isTablet) {
await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE);
} else {
await ScreenOrientation.unlockAsync();
}
} catch (e) {
logger.warn('[StreamsScreen] Pre-navigation orientation lock failed:', e);
}