mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 08:41:57 +00:00
some UI changes for ContinueWatching
This commit is contained in:
parent
afaca6467f
commit
9c533e3c52
8 changed files with 77 additions and 29 deletions
6
app.json
6
app.json
|
|
@ -2,7 +2,7 @@
|
||||||
"expo": {
|
"expo": {
|
||||||
"name": "Nuvio",
|
"name": "Nuvio",
|
||||||
"slug": "nuvio",
|
"slug": "nuvio",
|
||||||
"version": "0.6.0-beta.7",
|
"version": "0.6.0-beta.8",
|
||||||
"orientation": "default",
|
"orientation": "default",
|
||||||
"icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png",
|
"icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png",
|
||||||
"userInterfaceStyle": "dark",
|
"userInterfaceStyle": "dark",
|
||||||
|
|
@ -16,7 +16,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": "7",
|
"buildNumber": "8",
|
||||||
"infoPlist": {
|
"infoPlist": {
|
||||||
"NSAppTransportSecurity": {
|
"NSAppTransportSecurity": {
|
||||||
"NSAllowsArbitraryLoads": true
|
"NSAllowsArbitraryLoads": true
|
||||||
|
|
@ -46,7 +46,7 @@
|
||||||
"WAKE_LOCK"
|
"WAKE_LOCK"
|
||||||
],
|
],
|
||||||
"package": "com.nuvio.app",
|
"package": "com.nuvio.app",
|
||||||
"versionCode": 7,
|
"versionCode": 8,
|
||||||
"architectures": [
|
"architectures": [
|
||||||
"arm64-v8a",
|
"arm64-v8a",
|
||||||
"armeabi-v7a",
|
"armeabi-v7a",
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,10 @@ import { parseISO, isThisWeek, format, isAfter, isBefore } from 'date-fns';
|
||||||
import Animated, { FadeIn } from 'react-native-reanimated';
|
import Animated, { FadeIn } from 'react-native-reanimated';
|
||||||
import { useCalendarData } from '../../hooks/useCalendarData';
|
import { useCalendarData } from '../../hooks/useCalendarData';
|
||||||
|
|
||||||
|
// Compute base sizes; actual tablet sizes will be adjusted inside component for responsiveness
|
||||||
const { width } = Dimensions.get('window');
|
const { width } = Dimensions.get('window');
|
||||||
const ITEM_WIDTH = width * 0.75; // Reduced width for better spacing
|
const ITEM_WIDTH = width * 0.75; // phone default
|
||||||
const ITEM_HEIGHT = 180; // Compact height for cleaner design
|
const ITEM_HEIGHT = 180; // phone default
|
||||||
|
|
||||||
interface ThisWeekEpisode {
|
interface ThisWeekEpisode {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -57,6 +58,12 @@ export const ThisWeekSection = React.memo(() => {
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
const { calendarData, loading } = useCalendarData();
|
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 thisWeekEpisodes = useMemo(() => {
|
||||||
const thisWeekSection = calendarData.find(section => section.title === 'This Week');
|
const thisWeekSection = calendarData.find(section => section.title === 'This Week');
|
||||||
if (!thisWeekSection) return [];
|
if (!thisWeekSection) return [];
|
||||||
|
|
@ -109,7 +116,7 @@ export const ThisWeekSection = React.memo(() => {
|
||||||
item.poster);
|
item.poster);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.episodeItemContainer}>
|
<View style={[styles.episodeItemContainer, { width: computedItemWidth, height: computedItemHeight }]}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.episodeItem,
|
styles.episodeItem,
|
||||||
|
|
@ -143,30 +150,30 @@ export const ThisWeekSection = React.memo(() => {
|
||||||
>
|
>
|
||||||
{/* Content area */}
|
{/* Content area */}
|
||||||
<View style={styles.contentArea}>
|
<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}
|
{item.seriesName}
|
||||||
</Text>
|
</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}
|
{item.title}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{item.overview && (
|
{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}
|
{item.overview}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<View style={styles.dateContainer}>
|
<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} •
|
S{item.season}:E{item.episode} •
|
||||||
</Text>
|
</Text>
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name="event"
|
name="event"
|
||||||
size={14}
|
size={isTablet ? 16 : 14}
|
||||||
color={currentTheme.colors.primary}
|
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}
|
{formattedDate}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -197,10 +204,19 @@ export const ThisWeekSection = React.memo(() => {
|
||||||
renderItem={renderEpisodeItem}
|
renderItem={renderEpisodeItem}
|
||||||
horizontal
|
horizontal
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
contentContainerStyle={styles.listContent}
|
contentContainerStyle={[styles.listContent, { paddingLeft: isTablet ? 24 : 16, paddingRight: isTablet ? 24 : 16 }]}
|
||||||
snapToInterval={ITEM_WIDTH + 16}
|
snapToInterval={computedItemWidth + 16}
|
||||||
decelerationRate="fast"
|
decelerationRate="fast"
|
||||||
snapToAlignment="start"
|
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 }} />}
|
ItemSeparatorComponent={() => <View style={{ width: 16 }} />}
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
|
||||||
|
|
@ -799,11 +799,15 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
|
|
||||||
// Navigate immediately without delay
|
// Navigate immediately without delay
|
||||||
ScreenOrientation.unlockAsync().then(() => {
|
ScreenOrientation.unlockAsync().then(() => {
|
||||||
// On iOS, explicitly return to portrait to avoid sticking in landscape
|
// On tablets keep rotation unlocked; on phones, return to portrait
|
||||||
if (Platform.OS === 'ios') {
|
const { width: dw, height: dh } = Dimensions.get('window');
|
||||||
|
const isTablet = Math.min(dw, dh) >= 768 || ((Platform as any).isPad === true);
|
||||||
|
if (!isTablet) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => {});
|
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => {});
|
||||||
}, 50);
|
}, 50);
|
||||||
|
} else {
|
||||||
|
ScreenOrientation.unlockAsync().catch(() => {});
|
||||||
}
|
}
|
||||||
disableImmersiveMode();
|
disableImmersiveMode();
|
||||||
|
|
||||||
|
|
@ -815,11 +819,15 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
(navigation as any).navigate('Streams', { id, type, episodeId, fromPlayer: true });
|
(navigation as any).navigate('Streams', { id, type, episodeId, fromPlayer: true });
|
||||||
}
|
}
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
// Fallback: still try to restore portrait then navigate
|
// Fallback: still try to restore portrait on phones then navigate
|
||||||
if (Platform.OS === 'ios') {
|
const { width: dw, height: dh } = Dimensions.get('window');
|
||||||
|
const isTablet = Math.min(dw, dh) >= 768 || ((Platform as any).isPad === true);
|
||||||
|
if (!isTablet) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => {});
|
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => {});
|
||||||
}, 50);
|
}, 50);
|
||||||
|
} else {
|
||||||
|
ScreenOrientation.unlockAsync().catch(() => {});
|
||||||
}
|
}
|
||||||
disableImmersiveMode();
|
disableImmersiveMode();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -838,10 +838,16 @@ const VideoPlayer: React.FC = () => {
|
||||||
logger.warn('[VideoPlayer] Failed to unlock orientation:', orientationError);
|
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') {
|
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(() => {
|
setTimeout(() => {
|
||||||
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => {});
|
if (isTablet) {
|
||||||
|
ScreenOrientation.unlockAsync().catch(() => {});
|
||||||
|
} else {
|
||||||
|
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => {});
|
||||||
|
}
|
||||||
}, 50);
|
}, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
||||||
// Quality filtering defaults
|
// Quality filtering defaults
|
||||||
excludedQualities: [], // No qualities excluded by default
|
excludedQualities: [], // No qualities excluded by default
|
||||||
// Playback behavior defaults
|
// Playback behavior defaults
|
||||||
alwaysResume: false,
|
alwaysResume: true,
|
||||||
// Theme defaults
|
// Theme defaults
|
||||||
themeId: 'default',
|
themeId: 'default',
|
||||||
customThemes: [],
|
customThemes: [],
|
||||||
|
|
|
||||||
|
|
@ -369,9 +369,16 @@ const HomeScreen = () => {
|
||||||
StatusBar.setTranslucent(true);
|
StatusBar.setTranslucent(true);
|
||||||
StatusBar.setBackgroundColor('transparent');
|
StatusBar.setBackgroundColor('transparent');
|
||||||
|
|
||||||
// Ensure portrait when coming back to Home on all platforms
|
// Allow free rotation on tablets; lock portrait on phones
|
||||||
try {
|
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 {}
|
} catch {}
|
||||||
|
|
||||||
// For iOS specifically
|
// For iOS specifically
|
||||||
|
|
@ -569,7 +576,6 @@ const HomeScreen = () => {
|
||||||
// Normal flow when addons are present (featured moved to ListHeaderComponent)
|
// Normal flow when addons are present (featured moved to ListHeaderComponent)
|
||||||
|
|
||||||
data.push({ type: 'thisWeek', key: 'thisWeek' });
|
data.push({ type: 'thisWeek', key: 'thisWeek' });
|
||||||
data.push({ type: 'continueWatching', key: 'continueWatching' });
|
|
||||||
|
|
||||||
// Only show a limited number of catalogs initially for performance
|
// Only show a limited number of catalogs initially for performance
|
||||||
const catalogsToShow = catalogs.slice(0, visibleCatalogCount);
|
const catalogsToShow = catalogs.slice(0, visibleCatalogCount);
|
||||||
|
|
@ -631,6 +637,12 @@ const HomeScreen = () => {
|
||||||
|
|
||||||
const memoizedThisWeekSection = useMemo(() => <ThisWeekSection />, []);
|
const memoizedThisWeekSection = useMemo(() => <ThisWeekSection />, []);
|
||||||
const memoizedContinueWatchingSection = useMemo(() => <ContinueWatchingSection ref={continueWatchingRef} />, []);
|
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
|
// Track scroll direction manually for reliable behavior across platforms
|
||||||
const lastScrollYRef = useRef(0);
|
const lastScrollYRef = useRef(0);
|
||||||
const lastToggleRef = useRef(0);
|
const lastToggleRef = useRef(0);
|
||||||
|
|
@ -652,7 +664,7 @@ const HomeScreen = () => {
|
||||||
case 'thisWeek':
|
case 'thisWeek':
|
||||||
return wrapper(memoizedThisWeekSection);
|
return wrapper(memoizedThisWeekSection);
|
||||||
case 'continueWatching':
|
case 'continueWatching':
|
||||||
return wrapper(memoizedContinueWatchingSection);
|
return null; // Moved to ListHeaderComponent to avoid remounts on scroll
|
||||||
case 'catalog':
|
case 'catalog':
|
||||||
return wrapper(<CatalogSection catalog={item.catalog} />);
|
return wrapper(<CatalogSection catalog={item.catalog} />);
|
||||||
case 'placeholder':
|
case 'placeholder':
|
||||||
|
|
@ -749,7 +761,7 @@ const HomeScreen = () => {
|
||||||
{ paddingTop: insets.top }
|
{ paddingTop: insets.top }
|
||||||
]}
|
]}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
ListHeaderComponent={showHeroSection ? memoizedFeaturedContent : null}
|
ListHeaderComponent={memoizedHeader}
|
||||||
ListFooterComponent={ListFooterComponent}
|
ListFooterComponent={ListFooterComponent}
|
||||||
onMomentumScrollEnd={handleScrollEnd}
|
onMomentumScrollEnd={handleScrollEnd}
|
||||||
onEndReached={handleLoadMoreCatalogs}
|
onEndReached={handleLoadMoreCatalogs}
|
||||||
|
|
|
||||||
|
|
@ -582,7 +582,7 @@ const SettingsScreen: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
<SettingItem
|
<SettingItem
|
||||||
title="Version"
|
title="Version"
|
||||||
description="0.6.0-beta.7"
|
description="0.6.0-beta.8"
|
||||||
icon="info-outline"
|
icon="info-outline"
|
||||||
isLast={true}
|
isLast={true}
|
||||||
isTablet={isTablet}
|
isTablet={isTablet}
|
||||||
|
|
|
||||||
|
|
@ -940,9 +940,15 @@ export const StreamsScreen = () => {
|
||||||
logger.warn('[StreamsScreen] MKV support detection failed:', e);
|
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 {
|
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) {
|
} catch (e) {
|
||||||
logger.warn('[StreamsScreen] Pre-navigation orientation lock failed:', e);
|
logger.warn('[StreamsScreen] Pre-navigation orientation lock failed:', e);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue