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
3138f33fee
commit
c18f984eac
10 changed files with 504 additions and 238 deletions
|
|
@ -93,8 +93,8 @@ android {
|
|||
applicationId 'com.nuvio.app'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 11
|
||||
versionName "0.6.0-beta.11"
|
||||
versionCode 1
|
||||
versionName "1.0.0"
|
||||
}
|
||||
|
||||
splits {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
TouchableOpacity,
|
||||
Dimensions
|
||||
} from 'react-native';
|
||||
import { InteractionManager } from 'react-native';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { format, addMonths, subMonths, startOfMonth, endOfMonth, eachDayOfInterval, isSameMonth, isToday, isSameDay } from 'date-fns';
|
||||
import Animated, { FadeIn } from 'react-native-reanimated';
|
||||
|
|
@ -79,26 +80,35 @@ export const CalendarSection: React.FC<CalendarSectionProps> = ({
|
|||
const [currentDate, setCurrentDate] = useState(new Date());
|
||||
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
|
||||
const scrollViewRef = useRef<ScrollView>(null);
|
||||
const [uiReady, setUiReady] = useState(false);
|
||||
|
||||
// Map of dates with episodes
|
||||
const [datesWithEpisodes, setDatesWithEpisodes] = useState<{ [key: string]: boolean }>({});
|
||||
|
||||
// Process episodes to identify dates with content
|
||||
// Defer initial heavy work until after interactions
|
||||
useEffect(() => {
|
||||
if (__DEV__) console.log(`[CalendarSection] Processing ${episodes.length} episodes for calendar dots`);
|
||||
const task = InteractionManager.runAfterInteractions(() => setUiReady(true));
|
||||
return () => task.cancel();
|
||||
}, []);
|
||||
|
||||
// Process episodes to identify dates with content (bounded and deferred)
|
||||
useEffect(() => {
|
||||
if (!uiReady) return;
|
||||
const MAX_TO_PROCESS = 3000; // cap to prevent massive loops
|
||||
const dateMap: { [key: string]: boolean } = {};
|
||||
|
||||
episodes.forEach(episode => {
|
||||
if (episode.releaseDate) {
|
||||
const len = Math.min(episodes.length, MAX_TO_PROCESS);
|
||||
for (let i = 0; i < len; i++) {
|
||||
const episode = episodes[i];
|
||||
if (episode && episode.releaseDate) {
|
||||
const releaseDate = new Date(episode.releaseDate);
|
||||
const dateKey = format(releaseDate, 'yyyy-MM-dd');
|
||||
dateMap[dateKey] = true;
|
||||
if (!isNaN(releaseDate.getTime())) {
|
||||
const dateKey = format(releaseDate, 'yyyy-MM-dd');
|
||||
dateMap[dateKey] = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (__DEV__) console.log(`[CalendarSection] Found ${Object.keys(dateMap).length} unique dates with episodes`);
|
||||
}
|
||||
setDatesWithEpisodes(dateMap);
|
||||
}, [episodes]);
|
||||
}, [episodes, uiReady]);
|
||||
|
||||
const goToPreviousMonth = useCallback(() => {
|
||||
setCurrentDate(prev => subMonths(prev, 1));
|
||||
|
|
@ -213,9 +223,13 @@ export const CalendarSection: React.FC<CalendarSectionProps> = ({
|
|||
))}
|
||||
</View>
|
||||
|
||||
<View style={styles.daysContainer}>
|
||||
{renderDays()}
|
||||
</View>
|
||||
{uiReady ? (
|
||||
<View style={styles.daysContainer}>
|
||||
{renderDays()}
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.daysContainer} />
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,6 +10,12 @@ import { MaterialIcons } from '@expo/vector-icons';
|
|||
import { Image } from 'expo-image';
|
||||
import Animated, {
|
||||
FadeIn,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
runOnJS,
|
||||
interpolate,
|
||||
Extrapolate,
|
||||
} from 'react-native-reanimated';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import { isMDBListEnabled } from '../../screens/MDBListSettingsScreen';
|
||||
|
|
@ -36,6 +42,11 @@ const MetadataDetails: React.FC<MetadataDetailsProps> = ({
|
|||
const { currentTheme } = useTheme();
|
||||
const [isFullDescriptionOpen, setIsFullDescriptionOpen] = useState(false);
|
||||
const [isMDBEnabled, setIsMDBEnabled] = useState(false);
|
||||
const [isTextTruncated, setIsTextTruncated] = useState(false);
|
||||
|
||||
// Animation values for smooth height transition
|
||||
const animatedHeight = useSharedValue(0);
|
||||
const [measuredHeights, setMeasuredHeights] = useState({ collapsed: 0, expanded: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
const checkMDBListEnabled = async () => {
|
||||
|
|
@ -50,6 +61,42 @@ const MetadataDetails: React.FC<MetadataDetailsProps> = ({
|
|||
checkMDBListEnabled();
|
||||
}, []);
|
||||
|
||||
const handleTextLayout = (event: any) => {
|
||||
const { lines } = event.nativeEvent;
|
||||
// If we have 3 or more lines, it means the text was truncated
|
||||
setIsTextTruncated(lines.length >= 3);
|
||||
};
|
||||
|
||||
const handleCollapsedTextLayout = (event: any) => {
|
||||
const { height } = event.nativeEvent.layout;
|
||||
setMeasuredHeights(prev => ({ ...prev, collapsed: height }));
|
||||
};
|
||||
|
||||
const handleExpandedTextLayout = (event: any) => {
|
||||
const { height } = event.nativeEvent.layout;
|
||||
setMeasuredHeights(prev => ({ ...prev, expanded: height }));
|
||||
};
|
||||
|
||||
// Animate height changes
|
||||
const toggleDescription = () => {
|
||||
const targetHeight = isFullDescriptionOpen ? measuredHeights.collapsed : measuredHeights.expanded;
|
||||
animatedHeight.value = withTiming(targetHeight, { duration: 300 });
|
||||
setIsFullDescriptionOpen(!isFullDescriptionOpen);
|
||||
};
|
||||
|
||||
// Initialize height when component mounts or text changes
|
||||
useEffect(() => {
|
||||
if (measuredHeights.collapsed > 0) {
|
||||
animatedHeight.value = measuredHeights.collapsed;
|
||||
}
|
||||
}, [measuredHeights.collapsed]);
|
||||
|
||||
// Animated style for smooth height transition
|
||||
const animatedDescriptionStyle = useAnimatedStyle(() => ({
|
||||
height: animatedHeight.value,
|
||||
overflow: 'hidden',
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Metadata Source Selector removed */}
|
||||
|
|
@ -115,23 +162,47 @@ const MetadataDetails: React.FC<MetadataDetailsProps> = ({
|
|||
style={[styles.descriptionContainer, loadingMetadata && styles.dimmed]}
|
||||
entering={FadeIn.duration(300)}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={() => setIsFullDescriptionOpen(!isFullDescriptionOpen)}
|
||||
activeOpacity={0.7}
|
||||
{/* Hidden text elements to measure heights */}
|
||||
<Text
|
||||
style={[styles.description, { color: currentTheme.colors.mediumEmphasis, position: 'absolute', opacity: 0 }]}
|
||||
numberOfLines={3}
|
||||
onLayout={handleCollapsedTextLayout}
|
||||
>
|
||||
<Text style={[styles.description, { color: currentTheme.colors.mediumEmphasis }]} numberOfLines={isFullDescriptionOpen ? undefined : 3}>
|
||||
{metadata.description}
|
||||
</Text>
|
||||
<View style={styles.showMoreButton}>
|
||||
<Text style={[styles.showMoreText, { color: currentTheme.colors.textMuted }]}>
|
||||
{isFullDescriptionOpen ? 'Show Less' : 'Show More'}
|
||||
{metadata.description}
|
||||
</Text>
|
||||
<Text
|
||||
style={[styles.description, { color: currentTheme.colors.mediumEmphasis, position: 'absolute', opacity: 0 }]}
|
||||
onLayout={handleExpandedTextLayout}
|
||||
>
|
||||
{metadata.description}
|
||||
</Text>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={toggleDescription}
|
||||
activeOpacity={0.7}
|
||||
disabled={!isTextTruncated && !isFullDescriptionOpen}
|
||||
>
|
||||
<Animated.View style={animatedDescriptionStyle}>
|
||||
<Text
|
||||
style={[styles.description, { color: currentTheme.colors.mediumEmphasis }]}
|
||||
numberOfLines={isFullDescriptionOpen ? undefined : 3}
|
||||
onTextLayout={handleTextLayout}
|
||||
>
|
||||
{metadata.description}
|
||||
</Text>
|
||||
<MaterialIcons
|
||||
name={isFullDescriptionOpen ? "keyboard-arrow-up" : "keyboard-arrow-down"}
|
||||
size={18}
|
||||
color={currentTheme.colors.textMuted}
|
||||
/>
|
||||
</View>
|
||||
</Animated.View>
|
||||
{(isTextTruncated || isFullDescriptionOpen) && (
|
||||
<View style={styles.showMoreButton}>
|
||||
<Text style={[styles.showMoreText, { color: currentTheme.colors.textMuted }]}>
|
||||
{isFullDescriptionOpen ? 'Show Less' : 'Show More'}
|
||||
</Text>
|
||||
<MaterialIcons
|
||||
name={isFullDescriptionOpen ? "keyboard-arrow-up" : "keyboard-arrow-down"}
|
||||
size={18}
|
||||
color={currentTheme.colors.textMuted}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react';
|
||||
import { View, TouchableOpacity, TouchableWithoutFeedback, Dimensions, Animated, ActivityIndicator, Platform, NativeModules, StatusBar, Text, Image, StyleSheet, Modal } from 'react-native';
|
||||
import { View, TouchableOpacity, TouchableWithoutFeedback, Dimensions, Animated, ActivityIndicator, Platform, NativeModules, StatusBar, Text, Image, StyleSheet, Modal, AppState } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import Video, { VideoRef, SelectedTrack, SelectedTrackType, BufferingStrategyType } from 'react-native-video';
|
||||
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
||||
import { useNavigation, useRoute, RouteProp, useFocusEffect } from '@react-navigation/native';
|
||||
import { RootStackParamList } from '../../navigation/AppNavigator';
|
||||
import { PinchGestureHandler, PanGestureHandler, TapGestureHandler, State, PinchGestureHandlerGestureEvent, PanGestureHandlerGestureEvent, TapGestureHandlerGestureEvent } from 'react-native-gesture-handler';
|
||||
import RNImmersiveMode from 'react-native-immersive-mode';
|
||||
|
|
@ -588,6 +588,8 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
useEffect(() => {
|
||||
const subscription = Dimensions.addEventListener('change', ({ screen }) => {
|
||||
setScreenDimensions(screen);
|
||||
// Re-apply immersive mode on layout changes to keep system bars hidden
|
||||
enableImmersiveMode();
|
||||
});
|
||||
const initializePlayer = async () => {
|
||||
StatusBar.setHidden(true, 'none');
|
||||
|
|
@ -620,6 +622,27 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
};
|
||||
}, []);
|
||||
|
||||
// Re-apply immersive mode when screen gains focus
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
enableImmersiveMode();
|
||||
return () => {};
|
||||
}, [])
|
||||
);
|
||||
|
||||
// Re-apply immersive mode when app returns to foreground
|
||||
useEffect(() => {
|
||||
const onAppStateChange = (state: string) => {
|
||||
if (state === 'active') {
|
||||
enableImmersiveMode();
|
||||
}
|
||||
};
|
||||
const sub = AppState.addEventListener('change', onAppStateChange);
|
||||
return () => {
|
||||
sub.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const startOpeningAnimation = () => {
|
||||
// Logo entrance animation - optimized for faster appearance
|
||||
Animated.parallel([
|
||||
|
|
@ -1206,6 +1229,8 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
if (newShowControls) {
|
||||
controlsTimeout.current = setTimeout(hideControls, 5000);
|
||||
}
|
||||
// Reinforce immersive mode after any UI toggle
|
||||
enableImmersiveMode();
|
||||
return newShowControls;
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react';
|
||||
import { View, TouchableOpacity, Dimensions, Animated, ActivityIndicator, Platform, NativeModules, StatusBar, Text, Image, StyleSheet, Modal } from 'react-native';
|
||||
import { View, TouchableOpacity, Dimensions, Animated, ActivityIndicator, Platform, NativeModules, StatusBar, Text, Image, StyleSheet, Modal, AppState } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { VLCPlayer } from 'react-native-vlc-media-player';
|
||||
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
||||
import { useNavigation, useRoute, RouteProp, useFocusEffect } from '@react-navigation/native';
|
||||
import { RootStackParamList, RootStackNavigationProp } from '../../navigation/AppNavigator';
|
||||
import { PinchGestureHandler, PanGestureHandler, TapGestureHandler, State, PinchGestureHandlerGestureEvent, PanGestureHandlerGestureEvent, TapGestureHandlerGestureEvent } from 'react-native-gesture-handler';
|
||||
import RNImmersiveMode from 'react-native-immersive-mode';
|
||||
|
|
@ -567,6 +567,8 @@ const VideoPlayer: React.FC = () => {
|
|||
useEffect(() => {
|
||||
const subscription = Dimensions.addEventListener('change', ({ screen }) => {
|
||||
setScreenDimensions(screen);
|
||||
// Re-apply immersive mode on layout changes (Android)
|
||||
enableImmersiveMode();
|
||||
});
|
||||
const initializePlayer = async () => {
|
||||
StatusBar.setHidden(true, 'none');
|
||||
|
|
@ -599,6 +601,27 @@ const VideoPlayer: React.FC = () => {
|
|||
};
|
||||
}, []);
|
||||
|
||||
// Re-apply immersive mode when screen gains focus (Android)
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
enableImmersiveMode();
|
||||
return () => {};
|
||||
}, [])
|
||||
);
|
||||
|
||||
// Re-apply immersive mode when app returns to foreground (Android)
|
||||
useEffect(() => {
|
||||
const onAppStateChange = (state: string) => {
|
||||
if (state === 'active') {
|
||||
enableImmersiveMode();
|
||||
}
|
||||
};
|
||||
const sub = AppState.addEventListener('change', onAppStateChange);
|
||||
return () => {
|
||||
sub.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const startOpeningAnimation = () => {
|
||||
// Logo entrance animation - optimized for faster appearance
|
||||
Animated.parallel([
|
||||
|
|
@ -1166,6 +1189,8 @@ const VideoPlayer: React.FC = () => {
|
|||
if (newShowControls) {
|
||||
controlsTimeout.current = setTimeout(hideControls, 5000);
|
||||
}
|
||||
// Reinforce immersive mode after any UI toggle (Android)
|
||||
enableImmersiveMode();
|
||||
return newShowControls;
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -333,7 +333,8 @@ export const CustomNavigationDarkTheme: Theme = {
|
|||
type IconNameType = 'home' | 'home-outline' | 'compass' | 'compass-outline' |
|
||||
'play-box-multiple' | 'play-box-multiple-outline' |
|
||||
'puzzle' | 'puzzle-outline' |
|
||||
'cog' | 'cog-outline' | 'feature-search' | 'feature-search-outline';
|
||||
'cog' | 'cog-outline' | 'feature-search' | 'feature-search-outline' |
|
||||
'magnify' | 'heart' | 'heart-outline';
|
||||
|
||||
// Add TabIcon component
|
||||
const TabIcon = React.memo(({ focused, color, iconName }: {
|
||||
|
|
@ -352,7 +353,11 @@ const TabIcon = React.memo(({ focused, color, iconName }: {
|
|||
}).start();
|
||||
}, [focused]);
|
||||
|
||||
const finalIconName = focused ? iconName : `${iconName}-outline` as IconNameType;
|
||||
// Use outline variant when available, but keep single-form icons (like 'magnify') the same
|
||||
const finalIconName = (() => {
|
||||
if (iconName === 'magnify') return 'magnify';
|
||||
return focused ? iconName : `${iconName}-outline` as IconNameType;
|
||||
})();
|
||||
|
||||
return (
|
||||
<Animated.View style={{
|
||||
|
|
@ -637,10 +642,10 @@ const MainTabs = () => {
|
|||
iconName = 'home';
|
||||
break;
|
||||
case 'Library':
|
||||
iconName = 'play-box-multiple';
|
||||
iconName = 'heart';
|
||||
break;
|
||||
case 'Search':
|
||||
iconName = 'feature-search';
|
||||
iconName = 'magnify';
|
||||
break;
|
||||
case 'Settings':
|
||||
iconName = 'cog';
|
||||
|
|
@ -755,7 +760,7 @@ const MainTabs = () => {
|
|||
options={{
|
||||
tabBarLabel: 'Library',
|
||||
tabBarIcon: ({ color, size, focused }) => (
|
||||
<MaterialCommunityIcons name={focused ? 'play-box-multiple' : 'play-box-multiple-outline'} size={size} color={color} />
|
||||
<MaterialCommunityIcons name={focused ? 'heart' : 'heart-outline'} size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
|
@ -764,8 +769,8 @@ const MainTabs = () => {
|
|||
component={SearchScreen}
|
||||
options={{
|
||||
tabBarLabel: 'Search',
|
||||
tabBarIcon: ({ color, size, focused }) => (
|
||||
<MaterialCommunityIcons name={focused ? 'feature-search' : 'feature-search-outline'} size={size} color={color} />
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<MaterialCommunityIcons name={'magnify'} size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
SectionList,
|
||||
Platform
|
||||
} from 'react-native';
|
||||
import { InteractionManager } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NavigationProp } from '@react-navigation/native';
|
||||
import { Image } from 'expo-image';
|
||||
|
|
@ -69,6 +70,7 @@ const CalendarScreen = () => {
|
|||
|
||||
logger.log(`[Calendar] Initial load - Library has ${libraryItems?.length || 0} items, loading: ${libraryLoading}`);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [uiReady, setUiReady] = useState(false);
|
||||
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
|
||||
const [filteredEpisodes, setFilteredEpisodes] = useState<CalendarEpisode[]>([]);
|
||||
|
||||
|
|
@ -79,6 +81,14 @@ const CalendarScreen = () => {
|
|||
refresh(true);
|
||||
setRefreshing(false);
|
||||
}, [refresh]);
|
||||
|
||||
// Defer heavy UI work until after interactions to reduce jank/crashes
|
||||
useEffect(() => {
|
||||
const task = InteractionManager.runAfterInteractions(() => {
|
||||
setUiReady(true);
|
||||
});
|
||||
return () => task.cancel();
|
||||
}, []);
|
||||
|
||||
const handleSeriesPress = useCallback((seriesId: string, episode?: CalendarEpisode) => {
|
||||
navigation.navigate('Metadata', {
|
||||
|
|
@ -211,12 +221,15 @@ const CalendarScreen = () => {
|
|||
|
||||
// Process all episodes once data is loaded - using memory-efficient approach
|
||||
const allEpisodes = React.useMemo(() => {
|
||||
const episodes = calendarData.reduce((acc: CalendarEpisode[], section: CalendarSection) =>
|
||||
[...acc, ...section.data], [] as CalendarEpisode[]);
|
||||
|
||||
// Limit episodes to prevent memory issues in large libraries
|
||||
return memoryManager.limitArraySize(episodes, 1000);
|
||||
}, [calendarData]);
|
||||
if (!uiReady) return [] as CalendarEpisode[];
|
||||
const episodes = calendarData.reduce((acc: CalendarEpisode[], section: CalendarSection) => {
|
||||
// Pre-trim section arrays defensively
|
||||
const trimmed = memoryManager.limitArraySize(section.data, 500);
|
||||
return acc.length > 1500 ? acc : [...acc, ...trimmed];
|
||||
}, [] as CalendarEpisode[]);
|
||||
// Global cap to keep memory bounded
|
||||
return memoryManager.limitArraySize(episodes, 1500);
|
||||
}, [calendarData, uiReady]);
|
||||
|
||||
// Log when rendering with relevant state info
|
||||
logger.log(`[Calendar] Rendering: loading=${loading}, calendarData sections=${calendarData.length}, allEpisodes=${allEpisodes.length}`);
|
||||
|
|
@ -244,7 +257,7 @@ const CalendarScreen = () => {
|
|||
setFilteredEpisodes([]);
|
||||
}, []);
|
||||
|
||||
if (loading && !refreshing) {
|
||||
if ((loading || !uiReady) && !refreshing) {
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
<StatusBar barStyle="light-content" />
|
||||
|
|
@ -293,6 +306,11 @@ const CalendarScreen = () => {
|
|||
keyExtractor={(item) => item.id}
|
||||
renderItem={renderEpisodeItem}
|
||||
contentContainerStyle={styles.listContent}
|
||||
initialNumToRender={8}
|
||||
maxToRenderPerBatch={8}
|
||||
updateCellsBatchingPeriod={50}
|
||||
windowSize={7}
|
||||
removeClippedSubviews
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
|
|
@ -324,7 +342,11 @@ const CalendarScreen = () => {
|
|||
renderItem={renderEpisodeItem}
|
||||
renderSectionHeader={renderSectionHeader}
|
||||
contentContainerStyle={styles.listContent}
|
||||
removeClippedSubviews={false}
|
||||
removeClippedSubviews
|
||||
initialNumToRender={8}
|
||||
maxToRenderPerBatch={8}
|
||||
updateCellsBatchingPeriod={50}
|
||||
windowSize={7}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
ActivityIndicator,
|
||||
Platform,
|
||||
ScrollView,
|
||||
BackHandler,
|
||||
} from 'react-native';
|
||||
import { FlashList } from '@shopify/flash-list';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
|
@ -109,49 +110,33 @@ const TraktItem = React.memo(({ item, width, navigation, currentTheme }: { item:
|
|||
onPress={handlePress}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black }]}>
|
||||
{posterUrl ? (
|
||||
<Image
|
||||
source={{ uri: posterUrl }}
|
||||
style={styles.poster}
|
||||
contentFit="cover"
|
||||
cachePolicy="disk"
|
||||
transition={0}
|
||||
allowDownscaling
|
||||
/>
|
||||
) : (
|
||||
<View style={[styles.poster, { backgroundColor: currentTheme.colors.elevation1, justifyContent: 'center', alignItems: 'center' }]}>
|
||||
<ActivityIndicator color={currentTheme.colors.primary} />
|
||||
<View>
|
||||
<View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black }]}>
|
||||
{posterUrl ? (
|
||||
<Image
|
||||
source={{ uri: posterUrl }}
|
||||
style={styles.poster}
|
||||
contentFit="cover"
|
||||
cachePolicy="disk"
|
||||
transition={0}
|
||||
allowDownscaling
|
||||
/>
|
||||
) : (
|
||||
<View style={[styles.poster, { backgroundColor: currentTheme.colors.elevation1, justifyContent: 'center', alignItems: 'center' }]}>
|
||||
<ActivityIndicator color={currentTheme.colors.primary} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={[styles.badgeContainer, { backgroundColor: 'rgba(45,55,72,0.9)' }]}>
|
||||
<TraktIcon width={12} height={12} style={{ marginRight: 4 }} />
|
||||
<Text style={[styles.badgeText, { color: currentTheme.colors.white }]}>
|
||||
{item.type === 'movie' ? 'Movie' : 'Series'}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<LinearGradient
|
||||
colors={['transparent', 'rgba(0,0,0,0.85)']}
|
||||
style={styles.posterGradient}
|
||||
>
|
||||
<Text
|
||||
style={[styles.itemTitle, { color: currentTheme.colors.white }]}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
{item.lastWatched && (
|
||||
<Text style={styles.lastWatched}>
|
||||
Last watched: {item.lastWatched}
|
||||
</Text>
|
||||
)}
|
||||
{item.plays && item.plays > 1 && (
|
||||
<Text style={styles.playsCount}>
|
||||
{item.plays} plays
|
||||
</Text>
|
||||
)}
|
||||
</LinearGradient>
|
||||
|
||||
<View style={[styles.badgeContainer, { backgroundColor: 'rgba(232,37,75,0.9)' }]}>
|
||||
<TraktIcon width={12} height={12} style={{ marginRight: 4 }} />
|
||||
<Text style={[styles.badgeText, { color: currentTheme.colors.white }]}>
|
||||
{item.type === 'movie' ? 'Movie' : 'Series'}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={[styles.cardTitle, { color: currentTheme.colors.white }]}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
|
@ -255,14 +240,35 @@ const LibraryScreen = () => {
|
|||
StatusBar.setBackgroundColor('transparent');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
applyStatusBarConfig();
|
||||
|
||||
|
||||
// Re-apply on focus
|
||||
const unsubscribe = navigation.addListener('focus', applyStatusBarConfig);
|
||||
return unsubscribe;
|
||||
}, [navigation]);
|
||||
|
||||
// Handle hardware back button and gesture navigation
|
||||
useEffect(() => {
|
||||
const backAction = () => {
|
||||
if (showTraktContent) {
|
||||
if (selectedTraktFolder) {
|
||||
// If in a specific folder, go back to folder list
|
||||
setSelectedTraktFolder(null);
|
||||
} else {
|
||||
// If in Trakt collections view, go back to main library
|
||||
setShowTraktContent(false);
|
||||
}
|
||||
return true; // Prevent default back behavior
|
||||
}
|
||||
return false; // Allow default back behavior (navigate back)
|
||||
};
|
||||
|
||||
const backHandler = BackHandler.addEventListener('hardwareBackPress', backAction);
|
||||
|
||||
return () => backHandler.remove();
|
||||
}, [showTraktContent, selectedTraktFolder]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadLibrary = async () => {
|
||||
setLoading(true);
|
||||
|
|
@ -306,7 +312,7 @@ const LibraryScreen = () => {
|
|||
icon: 'visibility',
|
||||
description: 'Your watched content',
|
||||
itemCount: (watchedMovies?.length || 0) + (watchedShows?.length || 0),
|
||||
gradient: ['#4CAF50', '#2E7D32']
|
||||
gradient: ['#2C3E50', '#34495E']
|
||||
},
|
||||
{
|
||||
id: 'continue-watching',
|
||||
|
|
@ -314,7 +320,7 @@ const LibraryScreen = () => {
|
|||
icon: 'play-circle-outline',
|
||||
description: 'Resume your progress',
|
||||
itemCount: continueWatching?.length || 0,
|
||||
gradient: ['#FF9800', '#F57C00']
|
||||
gradient: ['#2980B9', '#3498DB']
|
||||
},
|
||||
{
|
||||
id: 'watchlist',
|
||||
|
|
@ -322,7 +328,7 @@ const LibraryScreen = () => {
|
|||
icon: 'bookmark',
|
||||
description: 'Want to watch',
|
||||
itemCount: (watchlistMovies?.length || 0) + (watchlistShows?.length || 0),
|
||||
gradient: ['#2196F3', '#1976D2']
|
||||
gradient: ['#6C3483', '#9B59B6']
|
||||
},
|
||||
{
|
||||
id: 'collection',
|
||||
|
|
@ -330,7 +336,7 @@ const LibraryScreen = () => {
|
|||
icon: 'library-add',
|
||||
description: 'Your collection',
|
||||
itemCount: (collectionMovies?.length || 0) + (collectionShows?.length || 0),
|
||||
gradient: ['#9C27B0', '#7B1FA2']
|
||||
gradient: ['#1B2631', '#283747']
|
||||
},
|
||||
{
|
||||
id: 'ratings',
|
||||
|
|
@ -338,7 +344,7 @@ const LibraryScreen = () => {
|
|||
icon: 'star',
|
||||
description: 'Your ratings',
|
||||
itemCount: ratedContent?.length || 0,
|
||||
gradient: ['#FF5722', '#D84315']
|
||||
gradient: ['#5D6D7E', '#85929E']
|
||||
}
|
||||
];
|
||||
|
||||
|
|
@ -352,51 +358,40 @@ const LibraryScreen = () => {
|
|||
onPress={() => navigation.navigate('Metadata', { id: item.id, type: item.type })}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black }]}>
|
||||
<Image
|
||||
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
|
||||
style={styles.poster}
|
||||
contentFit="cover"
|
||||
transition={300}
|
||||
/>
|
||||
<LinearGradient
|
||||
colors={['transparent', 'rgba(0,0,0,0.85)']}
|
||||
style={styles.posterGradient}
|
||||
>
|
||||
<Text
|
||||
style={[styles.itemTitle, { color: currentTheme.colors.white }]}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
{item.lastWatched && (
|
||||
<Text style={styles.lastWatched}>
|
||||
{item.lastWatched}
|
||||
</Text>
|
||||
<View>
|
||||
<View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black }]}>
|
||||
<Image
|
||||
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
|
||||
style={styles.poster}
|
||||
contentFit="cover"
|
||||
transition={300}
|
||||
/>
|
||||
|
||||
{item.progress !== undefined && item.progress < 1 && (
|
||||
<View style={styles.progressBarContainer}>
|
||||
<View
|
||||
style={[
|
||||
styles.progressBar,
|
||||
{ width: `${item.progress * 100}%`, backgroundColor: currentTheme.colors.primary }
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</LinearGradient>
|
||||
|
||||
{item.progress !== undefined && item.progress < 1 && (
|
||||
<View style={styles.progressBarContainer}>
|
||||
<View
|
||||
style={[
|
||||
styles.progressBar,
|
||||
{ width: `${item.progress * 100}%`, backgroundColor: currentTheme.colors.primary }
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
{item.type === 'series' && (
|
||||
<View style={styles.badgeContainer}>
|
||||
<MaterialIcons
|
||||
name="live-tv"
|
||||
size={14}
|
||||
color={currentTheme.colors.white}
|
||||
style={{ marginRight: 4 }}
|
||||
/>
|
||||
<Text style={[styles.badgeText, { color: currentTheme.colors.white }]}>Series</Text>
|
||||
</View>
|
||||
)}
|
||||
{item.type === 'series' && (
|
||||
<View style={styles.badgeContainer}>
|
||||
<MaterialIcons
|
||||
name="live-tv"
|
||||
size={14}
|
||||
color={currentTheme.colors.white}
|
||||
style={{ marginRight: 4 }}
|
||||
/>
|
||||
<Text style={[styles.badgeText, { color: currentTheme.colors.white }]}>Series</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Text style={[styles.cardTitle, { color: currentTheme.colors.white }]}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
|
@ -450,34 +445,31 @@ const LibraryScreen = () => {
|
|||
}}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={[styles.posterContainer, styles.folderContainer, { shadowColor: currentTheme.colors.black }]}>
|
||||
<LinearGradient
|
||||
colors={['#E8254B', '#C41E3A']}
|
||||
style={styles.folderGradient}
|
||||
>
|
||||
<TraktIcon width={60} height={60} style={{ marginBottom: 12 }} />
|
||||
<Text style={[styles.folderTitle, { color: currentTheme.colors.white }]}>
|
||||
Trakt Collection
|
||||
</Text>
|
||||
{traktAuthenticated && traktFolders.length > 0 && (
|
||||
<Text style={styles.folderCount}>
|
||||
{traktFolders.length} items
|
||||
<View>
|
||||
<View style={[styles.posterContainer, styles.folderContainer, { shadowColor: currentTheme.colors.black }]}>
|
||||
<LinearGradient
|
||||
colors={['#666666', '#444444']}
|
||||
style={styles.folderGradient}
|
||||
>
|
||||
<TraktIcon width={60} height={60} style={{ marginBottom: 12 }} />
|
||||
<Text style={[styles.folderTitle, { color: currentTheme.colors.white }]}>
|
||||
Trakt
|
||||
</Text>
|
||||
)}
|
||||
{!traktAuthenticated && (
|
||||
<Text style={styles.folderSubtitle}>
|
||||
Tap to connect
|
||||
</Text>
|
||||
)}
|
||||
</LinearGradient>
|
||||
|
||||
{/* Trakt badge */}
|
||||
<View style={[styles.badgeContainer, { backgroundColor: 'rgba(255,255,255,0.2)' }]}>
|
||||
<TraktIcon width={12} height={12} style={{ marginRight: 4 }} />
|
||||
<Text style={[styles.badgeText, { color: currentTheme.colors.white }]}>
|
||||
Trakt
|
||||
</Text>
|
||||
{traktAuthenticated && traktFolders.length > 0 && (
|
||||
<Text style={styles.folderCount}>
|
||||
{traktFolders.length} items
|
||||
</Text>
|
||||
)}
|
||||
{!traktAuthenticated && (
|
||||
<Text style={styles.folderSubtitle}>
|
||||
Tap to connect
|
||||
</Text>
|
||||
)}
|
||||
</LinearGradient>
|
||||
</View>
|
||||
<Text style={[styles.cardTitle, { color: currentTheme.colors.white }]}>
|
||||
Trakt collections
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
|
@ -921,7 +913,7 @@ const LibraryScreen = () => {
|
|||
<>
|
||||
<Text style={[styles.headerTitle, { color: currentTheme.colors.white }]}>Library</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.calendarButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||
style={styles.calendarButton}
|
||||
onPress={() => navigation.navigate('Calendar')}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
|
|
@ -1096,6 +1088,13 @@ const styles = StyleSheet.create({
|
|||
textShadowRadius: 2,
|
||||
letterSpacing: 0.3,
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
marginTop: 8,
|
||||
textAlign: 'center',
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
lastWatched: {
|
||||
fontSize: 12,
|
||||
color: 'rgba(255,255,255,0.7)',
|
||||
|
|
@ -1246,13 +1245,8 @@ const styles = StyleSheet.create({
|
|||
calendarButton: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
elevation: 3,
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 4,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import {
|
|||
StatusBar,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import Animated, {
|
||||
|
|
@ -20,6 +19,10 @@ import Animated, {
|
|||
withTiming,
|
||||
FadeInDown,
|
||||
FadeInUp,
|
||||
useAnimatedScrollHandler,
|
||||
runOnJS,
|
||||
interpolateColor,
|
||||
interpolate,
|
||||
} from 'react-native-reanimated';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { NavigationProp, useNavigation } from '@react-navigation/native';
|
||||
|
|
@ -71,6 +74,14 @@ const onboardingData: OnboardingSlide[] = [
|
|||
icon: 'library-books',
|
||||
gradient: ['#43e97b', '#38f9d7'],
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
title: 'Plugins',
|
||||
subtitle: 'Stream Sources Only',
|
||||
description: 'Plugins add streaming sources to Nuvio.',
|
||||
icon: 'widgets',
|
||||
gradient: ['#ff9a9e', '#fad0c4'],
|
||||
},
|
||||
];
|
||||
|
||||
const OnboardingScreen = () => {
|
||||
|
|
@ -80,6 +91,30 @@ const OnboardingScreen = () => {
|
|||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const flatListRef = useRef<FlatList>(null);
|
||||
const progressValue = useSharedValue(0);
|
||||
const scrollX = useSharedValue(0);
|
||||
const currentSlide = onboardingData[currentIndex];
|
||||
|
||||
const onScroll = useAnimatedScrollHandler({
|
||||
onScroll: (event) => {
|
||||
scrollX.value = event.contentOffset.x;
|
||||
},
|
||||
});
|
||||
|
||||
const getAnimatedBackgroundStyle = (slideIndex: number) => {
|
||||
return useAnimatedStyle(() => {
|
||||
const inputRange = [(slideIndex - 1) * width, slideIndex * width, (slideIndex + 1) * width];
|
||||
const opacity = interpolate(
|
||||
scrollX.value,
|
||||
inputRange,
|
||||
[0, 1, 0],
|
||||
'clamp'
|
||||
);
|
||||
|
||||
return {
|
||||
opacity,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const animatedProgressStyle = useAnimatedStyle(() => ({
|
||||
width: withSpring(`${((currentIndex + 1) / onboardingData.length) * 100}%`),
|
||||
|
|
@ -149,13 +184,13 @@ const OnboardingScreen = () => {
|
|||
entering={FadeInUp.delay(500).duration(800)}
|
||||
style={styles.textContainer}
|
||||
>
|
||||
<Text style={[styles.title, { color: currentTheme.colors.highEmphasis }]}>
|
||||
<Text style={[styles.title, { color: 'white' }]}>
|
||||
{item.title}
|
||||
</Text>
|
||||
<Text style={[styles.subtitle, { color: currentTheme.colors.primary }]}>
|
||||
<Text style={[styles.subtitle, { color: 'rgba(255,255,255,0.9)' }]}>
|
||||
{item.subtitle}
|
||||
</Text>
|
||||
<Text style={[styles.description, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
<Text style={[styles.description, { color: 'rgba(255,255,255,0.85)' }]}>
|
||||
{item.description}
|
||||
</Text>
|
||||
</Animated.View>
|
||||
|
|
@ -183,71 +218,106 @@ const OnboardingScreen = () => {
|
|||
);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
<StatusBar barStyle="light-content" backgroundColor={currentTheme.colors.darkBackground} />
|
||||
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={handleSkip} style={styles.skipButton}>
|
||||
<Text style={[styles.skipText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
Skip
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<View style={[styles.progressContainer, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.progressBar,
|
||||
{ backgroundColor: currentTheme.colors.primary },
|
||||
animatedProgressStyle
|
||||
]}
|
||||
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
{/* Layered animated gradient backgrounds */}
|
||||
{onboardingData.map((slide, index) => (
|
||||
<Animated.View key={`bg-${index}`} style={[styles.backgroundPanel, getAnimatedBackgroundStyle(index)]}>
|
||||
<LinearGradient
|
||||
colors={[slide.gradient[0], slide.gradient[1]]}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Content */}
|
||||
<FlatList
|
||||
ref={flatListRef}
|
||||
data={onboardingData}
|
||||
renderItem={renderSlide}
|
||||
horizontal
|
||||
pagingEnabled
|
||||
showsHorizontalScrollIndicator={false}
|
||||
keyExtractor={(item) => item.id}
|
||||
onMomentumScrollEnd={(event) => {
|
||||
const slideIndex = Math.round(event.nativeEvent.contentOffset.x / width);
|
||||
setCurrentIndex(slideIndex);
|
||||
}}
|
||||
style={{ flex: 1 }}
|
||||
</Animated.View>
|
||||
))}
|
||||
<LinearGradient
|
||||
colors={["rgba(0,0,0,0.2)", "rgba(0,0,0,0.45)"]}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 0, y: 1 }}
|
||||
style={styles.overlayPanel}
|
||||
/>
|
||||
{/* Decorative gradient blobs that change with current slide */}
|
||||
<LinearGradient
|
||||
colors={[currentSlide.gradient[1], 'transparent']}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
style={styles.blobTopRight}
|
||||
/>
|
||||
<LinearGradient
|
||||
colors={[currentSlide.gradient[0], 'transparent']}
|
||||
start={{ x: 1, y: 1 }}
|
||||
end={{ x: 0, y: 0 }}
|
||||
style={styles.blobBottomLeft}
|
||||
/>
|
||||
<StatusBar barStyle="light-content" backgroundColor="transparent" translucent />
|
||||
|
||||
{/* Footer */}
|
||||
<View style={styles.footer}>
|
||||
{renderPagination()}
|
||||
|
||||
<View style={styles.buttonContainer}>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.button,
|
||||
styles.nextButton,
|
||||
{ backgroundColor: currentTheme.colors.primary }
|
||||
]}
|
||||
onPress={handleNext}
|
||||
>
|
||||
<Text style={[styles.buttonText, { color: 'white' }]}>
|
||||
{currentIndex === onboardingData.length - 1 ? 'Get Started' : 'Next'}
|
||||
{/* Content container with status bar padding */}
|
||||
<View style={styles.fullScreenContainer}>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={handleSkip} style={styles.skipButton}>
|
||||
<Text style={[styles.skipText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
Skip
|
||||
</Text>
|
||||
<MaterialIcons
|
||||
name={currentIndex === onboardingData.length - 1 ? 'check' : 'arrow-forward'}
|
||||
size={20}
|
||||
color="white"
|
||||
style={styles.buttonIcon}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<View style={[styles.progressContainer, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.progressBar,
|
||||
{ backgroundColor: currentTheme.colors.primary },
|
||||
animatedProgressStyle
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Content */}
|
||||
<Animated.FlatList
|
||||
ref={flatListRef}
|
||||
data={onboardingData}
|
||||
renderItem={renderSlide}
|
||||
horizontal
|
||||
pagingEnabled
|
||||
showsHorizontalScrollIndicator={false}
|
||||
keyExtractor={(item) => item.id}
|
||||
onScroll={onScroll}
|
||||
scrollEventThrottle={16}
|
||||
onMomentumScrollEnd={(event) => {
|
||||
const slideIndex = Math.round(event.nativeEvent.contentOffset.x / width);
|
||||
setCurrentIndex(slideIndex);
|
||||
}}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
|
||||
{/* Footer */}
|
||||
<View style={styles.footer}>
|
||||
{renderPagination()}
|
||||
|
||||
<View style={styles.buttonContainer}>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.button,
|
||||
styles.nextButton,
|
||||
{ backgroundColor: currentTheme.colors.primary }
|
||||
]}
|
||||
onPress={handleNext}
|
||||
>
|
||||
<Text style={[styles.buttonText, { color: 'white' }]}>
|
||||
{currentIndex === onboardingData.length - 1 ? 'Get Started' : 'Next'}
|
||||
</Text>
|
||||
<MaterialIcons
|
||||
name={currentIndex === onboardingData.length - 1 ? 'check' : 'arrow-forward'}
|
||||
size={20}
|
||||
color="white"
|
||||
style={styles.buttonIcon}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -260,7 +330,7 @@ const styles = StyleSheet.create({
|
|||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: Platform.OS === 'ios' ? 10 : 20,
|
||||
paddingTop: 10,
|
||||
paddingBottom: 20,
|
||||
},
|
||||
skipButton: {
|
||||
|
|
@ -288,6 +358,46 @@ const styles = StyleSheet.create({
|
|||
justifyContent: 'center',
|
||||
paddingHorizontal: 40,
|
||||
},
|
||||
backgroundPanel: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
borderRadius: 0,
|
||||
},
|
||||
overlayPanel: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
borderRadius: 0,
|
||||
},
|
||||
fullScreenContainer: {
|
||||
flex: 1,
|
||||
paddingTop: Platform.OS === 'ios' ? 44 : StatusBar.currentHeight || 24,
|
||||
},
|
||||
blobTopRight: {
|
||||
position: 'absolute',
|
||||
top: -60,
|
||||
right: -60,
|
||||
width: 220,
|
||||
height: 220,
|
||||
borderRadius: 110,
|
||||
opacity: 0.35,
|
||||
transform: [{ rotate: '15deg' }],
|
||||
},
|
||||
blobBottomLeft: {
|
||||
position: 'absolute',
|
||||
bottom: -70,
|
||||
left: -70,
|
||||
width: 260,
|
||||
height: 260,
|
||||
borderRadius: 130,
|
||||
opacity: 0.28,
|
||||
transform: [{ rotate: '-20deg' }],
|
||||
},
|
||||
iconContainer: {
|
||||
width: 160,
|
||||
height: 160,
|
||||
|
|
|
|||
|
|
@ -606,7 +606,7 @@ const SettingsScreen: React.FC = () => {
|
|||
/>
|
||||
<SettingItem
|
||||
title="Version"
|
||||
description="0.6.0-beta.11"
|
||||
description="1.0.0"
|
||||
icon="info-outline"
|
||||
isLast={true}
|
||||
isTablet={isTablet}
|
||||
|
|
|
|||
Loading…
Reference in a new issue