major additions

This commit is contained in:
tapframe 2025-09-06 19:58:38 +05:30
parent e4bc0d3896
commit bf0fe2d5a1
14 changed files with 2633 additions and 1729 deletions

22
App.tsx
View file

@ -27,6 +27,8 @@ import { TraktProvider } from './src/contexts/TraktContext';
import { ThemeProvider, useTheme } from './src/contexts/ThemeContext';
import { TrailerProvider } from './src/contexts/TrailerContext';
import SplashScreen from './src/components/SplashScreen';
import UpdatePopup from './src/components/UpdatePopup';
import { useUpdatePopup } from './src/hooks/useUpdatePopup';
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as Sentry from '@sentry/react-native';
import UpdateService from './src/services/updateService';
@ -61,6 +63,16 @@ const ThemedApp = () => {
const [isAppReady, setIsAppReady] = useState(false);
const [hasCompletedOnboarding, setHasCompletedOnboarding] = useState<boolean | null>(null);
// Update popup functionality
const {
showUpdatePopup,
updateInfo,
isInstalling,
handleUpdateNow,
handleUpdateLater,
handleDismiss,
} = useUpdatePopup();
// Check onboarding status and initialize update service
useEffect(() => {
const initializeApp = async () => {
@ -122,6 +134,16 @@ const ThemedApp = () => {
/>
{!isAppReady && <SplashScreen onFinish={handleSplashComplete} />}
{shouldShowApp && <AppNavigator initialRouteName={initialRouteName} />}
{/* Update Popup */}
<UpdatePopup
visible={showUpdatePopup}
updateInfo={updateInfo}
onUpdateNow={handleUpdateNow}
onUpdateLater={handleUpdateLater}
onDismiss={handleDismiss}
isInstalling={isInstalling}
/>
</View>
</NavigationContainer>
</PaperProvider>

View file

@ -93,8 +93,8 @@ android {
applicationId 'com.nuvio.app'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 8
versionName "0.6.0-beta.8"
versionCode 9
versionName "0.6.0-beta.9"
}
splits {

View file

@ -3,5 +3,5 @@
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
<string name="expo_system_ui_user_interface_style" translatable="false">dark</string>
<string name="expo_runtime_version">0.6.0-beta.8</string>
<string name="expo_runtime_version">0.6.0-beta.9</string>
</resources>

View file

@ -2,7 +2,7 @@
"expo": {
"name": "Nuvio",
"slug": "nuvio",
"version": "0.6.0-beta.8",
"version": "0.6.0-beta.9",
"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": "8",
"buildNumber": "9",
"infoPlist": {
"NSAppTransportSecurity": {
"NSAllowsArbitraryLoads": true
@ -46,7 +46,7 @@
"WAKE_LOCK"
],
"package": "com.nuvio.app",
"versionCode": 8,
"versionCode": 9,
"architectures": [
"arm64-v8a",
"armeabi-v7a",
@ -54,9 +54,7 @@
"x86_64"
]
},
"web": {
"favicon": "./assets/favicon.png"
},
"extra": {
"eas": {
"projectId": "909107b8-fe61-45ce-b02f-b02510d306a6"
@ -86,6 +84,6 @@
"fallbackToCacheTimeout": 0,
"url": "https://grim-reyna-tapframe-69970143.koyeb.app/api/manifest"
},
"runtimeVersion": "0.6.0-beta.8"
"runtimeVersion": "0.6.0-beta.9"
}
}

View file

@ -1,5 +1,18 @@
import { registerRootComponent } from 'expo';
// Polyfill for Promise.allSettled (ES2020 feature)
if (!Promise.allSettled) {
Promise.allSettled = function<T>(promises: Promise<T>[]): Promise<PromiseSettledResult<T>[]> {
return Promise.all(
promises.map(promise =>
Promise.resolve(promise)
.then(value => ({ status: 'fulfilled' as const, value }))
.catch(reason => ({ status: 'rejected' as const, reason }))
)
);
};
}
import App from './App';
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);

2404
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -5,8 +5,7 @@
"scripts": {
"start": "expo start",
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web"
"ios": "expo run:ios"
},
"dependencies": {
"@backpackapp-io/react-native-toast": "^0.14.0",
@ -34,8 +33,8 @@
"eventemitter3": "^5.0.1",
"expo": "~52.0.47",
"expo-application": "~6.0.2",
"expo-auth-session": "^6.0.3",
"expo-blur": "^14.0.3",
"expo-auth-session": "~6.0.3",
"expo-blur": "~14.0.3",
"expo-dev-client": "~5.0.20",
"expo-device": "~7.0.3",
"expo-file-system": "~18.0.12",
@ -68,7 +67,8 @@
"react-native-url-polyfill": "^2.0.0",
"react-native-video": "^6.12.0",
"react-native-vlc-media-player": "^1.0.87",
"react-native-wheel-color-picker": "^1.3.1"
"react-native-wheel-color-picker": "^1.3.1",
"react-native-web": "~0.19.13"
},
"devDependencies": {
"@babel/core": "^7.25.2",

View file

@ -0,0 +1,361 @@
import React from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
Modal,
Dimensions,
Platform,
} from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import { useTheme } from '../contexts/ThemeContext';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import * as Haptics from 'expo-haptics';
const { width, height } = Dimensions.get('window');
interface UpdatePopupProps {
visible: boolean;
updateInfo: {
isAvailable: boolean;
manifest?: {
id: string;
version?: string;
description?: string;
};
};
onUpdateNow: () => void;
onUpdateLater: () => void;
onDismiss: () => void;
isInstalling?: boolean;
}
const UpdatePopup: React.FC<UpdatePopupProps> = ({
visible,
updateInfo,
onUpdateNow,
onUpdateLater,
onDismiss,
isInstalling = false,
}) => {
const { currentTheme } = useTheme();
const insets = useSafeAreaInsets();
const handleUpdateNow = () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
onUpdateNow();
};
const handleUpdateLater = () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
onUpdateLater();
};
const handleDismiss = () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
onDismiss();
};
if (!visible || !updateInfo.isAvailable) {
return null;
}
return (
<Modal
visible={visible}
transparent
animationType="fade"
statusBarTranslucent
>
<View style={styles.overlay}>
<View style={[
styles.popup,
{
backgroundColor: currentTheme.colors.elevation1 || '#1a1a1a',
borderColor: currentTheme.colors.elevation2 || '#333333',
marginTop: insets.top + 20,
marginBottom: insets.bottom + 20,
}
]}>
{/* Header */}
<View style={styles.header}>
<View style={[
styles.iconContainer,
{ backgroundColor: `${currentTheme.colors.primary}20` }
]}>
<MaterialIcons
name="system-update"
size={32}
color={currentTheme.colors.primary}
/>
</View>
<Text style={[
styles.title,
{ color: currentTheme.colors.highEmphasis }
]}>
Update Available
</Text>
<Text style={[
styles.subtitle,
{ color: currentTheme.colors.mediumEmphasis }
]}>
A new version of Nuvio is ready to install
</Text>
</View>
{/* Update Info */}
<View style={styles.updateInfo}>
<View style={styles.infoRow}>
<MaterialIcons
name="info-outline"
size={16}
color={currentTheme.colors.primary}
/>
<Text style={[
styles.infoLabel,
{ color: currentTheme.colors.mediumEmphasis }
]}>
Version:
</Text>
<Text style={[
styles.infoValue,
{ color: currentTheme.colors.highEmphasis }
]}>
{updateInfo.manifest?.id ?
`${updateInfo.manifest.id.substring(0, 8)}...` :
'Latest'
}
</Text>
</View>
{updateInfo.manifest?.description && (
<View style={styles.descriptionContainer}>
<Text style={[
styles.description,
{ color: currentTheme.colors.mediumEmphasis }
]}>
{updateInfo.manifest.description}
</Text>
</View>
)}
</View>
{/* Actions */}
<View style={styles.actions}>
<TouchableOpacity
style={[
styles.button,
styles.primaryButton,
{ backgroundColor: currentTheme.colors.primary },
isInstalling && styles.disabledButton
]}
onPress={handleUpdateNow}
disabled={isInstalling}
activeOpacity={0.8}
>
{isInstalling ? (
<>
<MaterialIcons name="install-mobile" size={18} color="white" />
<Text style={styles.buttonText}>Installing...</Text>
</>
) : (
<>
<MaterialIcons name="download" size={18} color="white" />
<Text style={styles.buttonText}>Update Now</Text>
</>
)}
</TouchableOpacity>
<View style={styles.secondaryActions}>
<TouchableOpacity
style={[
styles.button,
styles.secondaryButton,
{
backgroundColor: currentTheme.colors.elevation2,
borderColor: currentTheme.colors.elevation3,
}
]}
onPress={handleUpdateLater}
disabled={isInstalling}
activeOpacity={0.7}
>
<Text style={[
styles.secondaryButtonText,
{ color: currentTheme.colors.mediumEmphasis }
]}>
Later
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.button,
styles.secondaryButton,
{
backgroundColor: currentTheme.colors.elevation2,
borderColor: currentTheme.colors.elevation3,
}
]}
onPress={handleDismiss}
disabled={isInstalling}
activeOpacity={0.7}
>
<Text style={[
styles.secondaryButtonText,
{ color: currentTheme.colors.mediumEmphasis }
]}>
Dismiss
</Text>
</TouchableOpacity>
</View>
</View>
{/* Footer */}
<View style={styles.footer}>
<Text style={[
styles.footerText,
{ color: currentTheme.colors.textMuted }
]}>
Updates include bug fixes and new features
</Text>
</View>
</View>
</View>
</Modal>
);
};
const styles = StyleSheet.create({
overlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.8)',
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 20,
},
popup: {
width: Math.min(width - 40, 400),
borderRadius: 20,
borderWidth: 1,
backgroundColor: '#1a1a1a', // Fallback solid background
shadowColor: '#000',
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.5,
shadowRadius: 20,
elevation: 15,
overflow: 'hidden',
},
header: {
alignItems: 'center',
paddingHorizontal: 24,
paddingTop: 32,
paddingBottom: 20,
},
iconContainer: {
width: 64,
height: 64,
borderRadius: 32,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 16,
},
title: {
fontSize: 24,
fontWeight: '700',
letterSpacing: 0.3,
marginBottom: 8,
textAlign: 'center',
},
subtitle: {
fontSize: 16,
textAlign: 'center',
lineHeight: 22,
},
updateInfo: {
paddingHorizontal: 24,
paddingBottom: 20,
},
infoRow: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 12,
},
infoLabel: {
fontSize: 14,
fontWeight: '500',
marginLeft: 8,
marginRight: 8,
},
infoValue: {
fontSize: 14,
fontWeight: '600',
flex: 1,
},
descriptionContainer: {
marginTop: 8,
padding: 12,
borderRadius: 8,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
},
description: {
fontSize: 14,
lineHeight: 20,
},
actions: {
paddingHorizontal: 24,
paddingBottom: 20,
},
button: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 14,
paddingHorizontal: 20,
borderRadius: 12,
gap: 8,
marginBottom: 12,
},
primaryButton: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.2,
shadowRadius: 8,
elevation: 4,
},
secondaryButton: {
borderWidth: 1,
flex: 1,
marginHorizontal: 4,
},
disabledButton: {
opacity: 0.6,
},
buttonText: {
color: 'white',
fontSize: 16,
fontWeight: '600',
letterSpacing: 0.3,
},
secondaryActions: {
flexDirection: 'row',
gap: 8,
},
secondaryButtonText: {
fontSize: 15,
fontWeight: '500',
},
footer: {
paddingHorizontal: 24,
paddingBottom: 24,
alignItems: 'center',
},
footerText: {
fontSize: 12,
textAlign: 'center',
opacity: 0.7,
},
});
export default UpdatePopup;

View file

@ -758,9 +758,9 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
// Ensure trailer state is properly synchronized when trailer becomes ready
useEffect(() => {
if (trailerReady && settings?.showTrailers && !globalTrailerPlaying) {
// If trailer is ready but not playing, start it
logger.info('HeroSection', 'Starting trailer after it became ready');
setTrailerPlaying(true);
// Only start trailer if it's the initial load, not when returning from other screens
// This prevents auto-starting when returning from StreamsScreen
logger.info('HeroSection', 'Trailer ready but not playing - not auto-starting to prevent unwanted playback');
}
}, [trailerReady, settings?.showTrailers, globalTrailerPlaying, setTrailerPlaying]);
@ -1033,29 +1033,23 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
return () => subscription?.remove();
}, [setTrailerPlaying, globalTrailerPlaying]);
// Navigation focus effect - improved to prevent trailer interruption
// Navigation focus effect - conservative approach to prevent unwanted trailer resumption
useFocusEffect(
useCallback(() => {
// Screen is focused - ensure trailer can play if it should be playing
// Screen is focused - only resume trailer if it was previously playing and got interrupted
logger.info('HeroSection', 'Screen focused');
// Small delay to ensure the screen is fully focused before checking trailer state
const focusTimer = setTimeout(() => {
// If trailer should be playing but isn't, resume it
if (settings?.showTrailers && trailerReady && !globalTrailerPlaying) {
logger.info('HeroSection', 'Resuming trailer after screen focus');
setTrailerPlaying(true);
}
}, 100);
// Don't automatically resume trailer when returning from other screens
// This prevents the trailer from starting when returning from StreamsScreen
// The trailer should only resume if the user explicitly wants it to play
return () => {
clearTimeout(focusTimer);
// Don't automatically stop trailer when component unmounts
// Let the new hero section (if any) take control of trailer state
// This prevents the trailer from stopping when navigating between screens
logger.info('HeroSection', 'Screen unfocused - not stopping trailer automatically');
};
}, [settings?.showTrailers, trailerReady, globalTrailerPlaying, setTrailerPlaying])
}, [])
);
// Memory management and cleanup

161
src/hooks/useUpdatePopup.ts Normal file
View file

@ -0,0 +1,161 @@
import { useState, useEffect, useCallback } from 'react';
import { Alert } from 'react-native';
import UpdateService from '../services/updateService';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface UpdateInfo {
isAvailable: boolean;
manifest?: {
id: string;
version?: string;
description?: string;
};
}
interface UseUpdatePopupReturn {
showUpdatePopup: boolean;
updateInfo: UpdateInfo;
isInstalling: boolean;
checkForUpdates: () => Promise<void>;
handleUpdateNow: () => Promise<void>;
handleUpdateLater: () => void;
handleDismiss: () => void;
}
const UPDATE_POPUP_STORAGE_KEY = '@update_popup_dismissed';
const UPDATE_LATER_STORAGE_KEY = '@update_later_timestamp';
export const useUpdatePopup = (): UseUpdatePopupReturn => {
const [showUpdatePopup, setShowUpdatePopup] = useState(false);
const [updateInfo, setUpdateInfo] = useState<UpdateInfo>({ isAvailable: false });
const [isInstalling, setIsInstalling] = useState(false);
const checkForUpdates = useCallback(async () => {
try {
// Check if user has dismissed the popup for this version
const dismissedVersion = await AsyncStorage.getItem(UPDATE_POPUP_STORAGE_KEY);
const currentVersion = updateInfo.manifest?.id;
if (dismissedVersion === currentVersion) {
return; // User already dismissed this version
}
// Check if user chose "later" recently (within 24 hours)
const updateLaterTimestamp = await AsyncStorage.getItem(UPDATE_LATER_STORAGE_KEY);
if (updateLaterTimestamp) {
const laterTime = parseInt(updateLaterTimestamp);
const now = Date.now();
const twentyFourHours = 24 * 60 * 60 * 1000;
if (now - laterTime < twentyFourHours) {
return; // User chose "later" recently
}
}
const info = await UpdateService.checkForUpdates();
setUpdateInfo(info);
if (info.isAvailable) {
setShowUpdatePopup(true);
}
} catch (error) {
console.error('Error checking for updates:', error);
// Don't show popup on error, just log it
}
}, [updateInfo.manifest?.id]);
const handleUpdateNow = useCallback(async () => {
try {
setIsInstalling(true);
setShowUpdatePopup(false);
const success = await UpdateService.downloadAndInstallUpdate();
if (success) {
Alert.alert(
'Update Installed',
'The update has been installed successfully. Please restart the app to apply the changes.',
[
{
text: 'Restart Later',
style: 'cancel',
},
{
text: 'Restart Now',
onPress: () => {
// On React Native, we can't programmatically restart the app
// The user will need to manually restart
Alert.alert(
'Restart Required',
'Please close and reopen the app to complete the update.'
);
},
},
]
);
} else {
Alert.alert(
'Update Failed',
'Unable to install the update. Please try again later or check your internet connection.'
);
// Show popup again after failed installation
setShowUpdatePopup(true);
}
} catch (error) {
console.error('Error installing update:', error);
Alert.alert(
'Update Error',
'An error occurred while installing the update. Please try again later.'
);
// Show popup again after error
setShowUpdatePopup(true);
} finally {
setIsInstalling(false);
}
}, []);
const handleUpdateLater = useCallback(async () => {
try {
// Store timestamp when user chose "later"
await AsyncStorage.setItem(UPDATE_LATER_STORAGE_KEY, Date.now().toString());
setShowUpdatePopup(false);
} catch (error) {
console.error('Error storing update later preference:', error);
setShowUpdatePopup(false);
}
}, []);
const handleDismiss = useCallback(async () => {
try {
// Store the current version ID so we don't show popup again for this version
const currentVersion = updateInfo.manifest?.id;
if (currentVersion) {
await AsyncStorage.setItem(UPDATE_POPUP_STORAGE_KEY, currentVersion);
}
setShowUpdatePopup(false);
} catch (error) {
console.error('Error storing dismiss preference:', error);
setShowUpdatePopup(false);
}
}, [updateInfo.manifest?.id]);
// Auto-check for updates when hook is first used
useEffect(() => {
// Add a small delay to ensure the app is fully loaded
const timer = setTimeout(() => {
checkForUpdates();
}, 2000); // 2 second delay
return () => clearTimeout(timer);
}, [checkForUpdates]);
return {
showUpdatePopup,
updateInfo,
isInstalling,
checkForUpdates,
handleUpdateNow,
handleUpdateLater,
handleDismiss,
};
};

View file

@ -47,6 +47,7 @@ import { AccountProvider, useAccount } from '../contexts/AccountContext';
import { LoadingProvider, useLoading } from '../contexts/LoadingContext';
import PluginsScreen from '../screens/PluginsScreen';
import CastMoviesScreen from '../screens/CastMoviesScreen';
import UpdateScreen from '../screens/UpdateScreen';
// Stack navigator types
export type RootStackParamList = {
@ -55,6 +56,7 @@ export type RootStackParamList = {
Home: undefined;
Library: undefined;
Settings: undefined;
Update: undefined;
Search: undefined;
Calendar: undefined;
Metadata: {
@ -1196,6 +1198,21 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
},
}}
/>
<Stack.Screen
name="Update"
component={UpdateScreen}
options={{
animation: Platform.OS === 'android' ? 'slide_from_right' : 'slide_from_right',
animationDuration: Platform.OS === 'android' ? 250 : 300,
presentation: 'card',
gestureEnabled: true,
gestureDirection: 'horizontal',
headerShown: false,
contentStyle: {
backgroundColor: currentTheme.colors.darkBackground,
},
}}
/>
</Stack.Navigator>
</View>
</PaperProvider>

View file

@ -19,7 +19,7 @@ import {
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useNavigation } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import { MaterialIcons } from '@expo/vector-icons';
import { Picker } from '@react-native-picker/picker';
import { useSettings, DEFAULT_SETTINGS } from '../hooks/useSettings';
import { RootStackParamList } from '../navigation/AppNavigator';
@ -31,7 +31,6 @@ import { useAccount } from '../contexts/AccountContext';
import { catalogService } from '../services/catalogService';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import * as Sentry from '@sentry/react-native';
import UpdateService from '../services/updateService';
const { width, height } = Dimensions.get('window');
const isTablet = width >= 768;
@ -40,15 +39,15 @@ const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
// Settings categories for tablet sidebar
const SETTINGS_CATEGORIES = [
{ id: 'account', title: 'Account', icon: 'account-circle' },
{ id: 'content', title: 'Content & Discovery', icon: 'explore' },
{ id: 'appearance', title: 'Appearance', icon: 'palette' },
{ id: 'integrations', title: 'Integrations', icon: 'extension' },
{ id: 'playback', title: 'Playback', icon: 'play-circle-outline' },
{ id: 'updates', title: 'Updates', icon: 'system-update' },
{ id: 'about', title: 'About', icon: 'info-outline' },
{ id: 'developer', title: 'Developer', icon: 'code' },
{ id: 'cache', title: 'Cache', icon: 'cached' },
{ id: 'account', title: 'Account', icon: 'account-circle' as keyof typeof MaterialIcons.glyphMap },
{ id: 'content', title: 'Content & Discovery', icon: 'explore' as keyof typeof MaterialIcons.glyphMap },
{ id: 'appearance', title: 'Appearance', icon: 'palette' as keyof typeof MaterialIcons.glyphMap },
{ id: 'integrations', title: 'Integrations', icon: 'extension' as keyof typeof MaterialIcons.glyphMap },
{ id: 'playback', title: 'Playback', icon: 'play-circle-outline' as keyof typeof MaterialIcons.glyphMap },
{ id: 'updates', title: 'Updates', icon: 'system-update' as keyof typeof MaterialIcons.glyphMap },
{ id: 'about', title: 'About', icon: 'info-outline' as keyof typeof MaterialIcons.glyphMap },
{ id: 'developer', title: 'Developer', icon: 'code' as keyof typeof MaterialIcons.glyphMap },
{ id: 'cache', title: 'Cache', icon: 'cached' as keyof typeof MaterialIcons.glyphMap },
];
// Card component with minimalistic style
@ -91,7 +90,7 @@ const SettingsCard: React.FC<SettingsCardProps> = ({ children, title, isTablet =
interface SettingItemProps {
title: string;
description?: string;
icon: string;
icon: keyof typeof MaterialIcons.glyphMap;
renderControl?: () => React.ReactNode;
isLast?: boolean;
onPress?: () => void;
@ -223,347 +222,6 @@ const Sidebar: React.FC<SidebarProps> = ({ selectedCategory, onCategorySelect, c
);
};
// Updates Section Component
interface UpdatesSectionProps {
isTablet: boolean;
}
const UpdatesSection: React.FC<UpdatesSectionProps> = ({ isTablet }) => {
const { currentTheme } = useTheme();
const [updateInfo, setUpdateInfo] = useState<any>(null);
const [isChecking, setIsChecking] = useState(false);
const [isInstalling, setIsInstalling] = useState(false);
const [lastChecked, setLastChecked] = useState<Date | null>(null);
const [logs, setLogs] = useState<string[]>([]);
const [showLogs, setShowLogs] = useState(false);
const [lastOperation, setLastOperation] = useState<string>('');
const checkForUpdates = async () => {
try {
setIsChecking(true);
setLastOperation('Checking for updates...');
const info = await UpdateService.checkForUpdates();
setUpdateInfo(info);
setLastChecked(new Date());
// Refresh logs after operation
const logs = UpdateService.getLogs();
setLogs(logs);
if (info.isAvailable) {
setLastOperation(`Update available: ${info.manifest?.id?.substring(0, 8) || 'unknown'}...`);
} else {
setLastOperation('No updates available');
}
} catch (error) {
console.error('Error checking for updates:', error);
setLastOperation(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
Alert.alert('Error', 'Failed to check for updates');
} finally {
setIsChecking(false);
}
};
const installUpdate = async () => {
try {
setIsInstalling(true);
setLastOperation('Installing update...');
const success = await UpdateService.downloadAndInstallUpdate();
// Refresh logs after operation
const logs = UpdateService.getLogs();
setLogs(logs);
if (success) {
setLastOperation('Update installed successfully');
Alert.alert('Success', 'Update will be applied on next app restart');
} else {
setLastOperation('No update available to install');
Alert.alert('No Update', 'No update available to install');
}
} catch (error) {
console.error('Error installing update:', error);
setLastOperation(`Installation error: ${error instanceof Error ? error.message : 'Unknown error'}`);
Alert.alert('Error', 'Failed to install update');
} finally {
setIsInstalling(false);
}
};
const getCurrentUpdateInfo = async () => {
const info = await UpdateService.getCurrentUpdateInfo();
setUpdateInfo(info);
const logs = UpdateService.getLogs();
setLogs(logs);
};
const refreshLogs = () => {
const logs = UpdateService.getLogs();
setLogs(logs);
};
const clearLogs = () => {
UpdateService.clearLogs();
setLogs([]);
setLastOperation('Logs cleared');
};
const copyLog = (logText: string) => {
Clipboard.setString(logText);
Alert.alert('Copied', 'Log entry copied to clipboard');
};
const copyAllLogs = () => {
const allLogsText = logs.join('\n');
Clipboard.setString(allLogsText);
Alert.alert('Copied', 'All logs copied to clipboard');
};
const addTestLog = () => {
UpdateService.addTestLog(`Test log entry at ${new Date().toISOString()}`);
const logs = UpdateService.getLogs();
setLogs(logs);
setLastOperation('Test log added');
};
const testConnectivity = async () => {
try {
setLastOperation('Testing connectivity...');
const isReachable = await UpdateService.testUpdateConnectivity();
const logs = UpdateService.getLogs();
setLogs(logs);
if (isReachable) {
setLastOperation('Update server is reachable');
} else {
setLastOperation('Update server is not reachable');
}
} catch (error) {
console.error('Error testing connectivity:', error);
setLastOperation(`Connectivity test error: ${error instanceof Error ? error.message : 'Unknown error'}`);
const logs = UpdateService.getLogs();
setLogs(logs);
}
};
const testAssetUrls = async () => {
try {
setLastOperation('Testing asset URLs...');
await UpdateService.testAllAssetUrls();
const logs = UpdateService.getLogs();
setLogs(logs);
setLastOperation('Asset URL testing completed');
} catch (error) {
console.error('Error testing asset URLs:', error);
setLastOperation(`Asset URL test error: ${error instanceof Error ? error.message : 'Unknown error'}`);
const logs = UpdateService.getLogs();
setLogs(logs);
}
};
// Load current update info on mount
useEffect(() => {
const loadInitialData = async () => {
await getCurrentUpdateInfo();
// Also refresh logs to ensure we have the latest
refreshLogs();
};
loadInitialData();
}, []);
const formatDate = (date: Date) => {
return date.toLocaleString();
};
return (
<View>
<SettingsCard title="APP UPDATES" isTablet={isTablet}>
<SettingItem
title="Check for Updates"
description={isChecking ? "Checking..." : lastOperation || "Manually check for new updates"}
icon="system-update"
onPress={checkForUpdates}
renderControl={() => (
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
{isChecking && (
<View style={[styles.loadingSpinner, { borderColor: currentTheme.colors.primary }]} />
)}
<MaterialIcons
name="chevron-right"
size={24}
color={currentTheme.colors.mediumEmphasis}
/>
</View>
)}
isTablet={isTablet}
/>
{updateInfo?.isAvailable && (
<SettingItem
title="Install Update"
description={isInstalling ? "Installing..." : "Download and install the latest update"}
icon="download"
onPress={installUpdate}
renderControl={() => (
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
{isInstalling && (
<View style={[styles.loadingSpinner, { borderColor: currentTheme.colors.primary }]} />
)}
<MaterialIcons
name="chevron-right"
size={24}
color={currentTheme.colors.mediumEmphasis}
/>
</View>
)}
isTablet={isTablet}
/>
)}
<SettingItem
title="Current Version"
description={updateInfo?.manifest?.id ? `Update ID: ${updateInfo.manifest.id.substring(0, 8)}...` : "App version info"}
icon="info"
isTablet={isTablet}
/>
{lastChecked && (
<SettingItem
title="Last Checked"
description={formatDate(lastChecked)}
icon="schedule"
isTablet={isTablet}
/>
)}
<SettingItem
title="Update Logs"
description={`${logs.length} log entries`}
icon="history"
onPress={() => setShowLogs(!showLogs)}
renderControl={() => (
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<MaterialIcons
name={showLogs ? "expand-less" : "expand-more"}
size={24}
color={currentTheme.colors.mediumEmphasis}
/>
</View>
)}
isTablet={isTablet}
/>
</SettingsCard>
{showLogs && (
<SettingsCard title="UPDATE LOGS" isTablet={isTablet}>
<View style={styles.logsContainer}>
<View style={styles.logsHeader}>
<Text style={[styles.logsHeaderText, { color: currentTheme.colors.highEmphasis }]}>
Update Service Logs
</Text>
<View style={styles.logsActions}>
<TouchableOpacity
style={[styles.logActionButton, { backgroundColor: currentTheme.colors.elevation2 }]}
onPress={testConnectivity}
activeOpacity={0.7}
>
<MaterialIcons name="wifi" size={16} color={currentTheme.colors.primary} />
</TouchableOpacity>
<TouchableOpacity
style={[styles.logActionButton, { backgroundColor: currentTheme.colors.elevation2 }]}
onPress={testAssetUrls}
activeOpacity={0.7}
>
<MaterialIcons name="link" size={16} color={currentTheme.colors.primary} />
</TouchableOpacity>
<TouchableOpacity
style={[styles.logActionButton, { backgroundColor: currentTheme.colors.elevation2 }]}
onPress={addTestLog}
activeOpacity={0.7}
>
<MaterialIcons name="add" size={16} color={currentTheme.colors.primary} />
</TouchableOpacity>
<TouchableOpacity
style={[styles.logActionButton, { backgroundColor: currentTheme.colors.elevation2 }]}
onPress={copyAllLogs}
activeOpacity={0.7}
>
<MaterialIcons name="content-copy" size={16} color={currentTheme.colors.primary} />
</TouchableOpacity>
<TouchableOpacity
style={[styles.logActionButton, { backgroundColor: currentTheme.colors.elevation2 }]}
onPress={refreshLogs}
activeOpacity={0.7}
>
<MaterialIcons name="refresh" size={16} color={currentTheme.colors.primary} />
</TouchableOpacity>
<TouchableOpacity
style={[styles.logActionButton, { backgroundColor: currentTheme.colors.elevation2 }]}
onPress={clearLogs}
activeOpacity={0.7}
>
<MaterialIcons name="clear" size={16} color={currentTheme.colors.error || '#ff4444'} />
</TouchableOpacity>
</View>
</View>
<ScrollView
style={[styles.logsScrollView, { backgroundColor: currentTheme.colors.elevation2 }]}
showsVerticalScrollIndicator={true}
nestedScrollEnabled={true}
>
{logs.length === 0 ? (
<Text style={[styles.noLogsText, { color: currentTheme.colors.mediumEmphasis }]}>
No logs available
</Text>
) : (
logs.map((log, index) => {
const isError = log.includes('[ERROR]');
const isWarning = log.includes('[WARN]');
return (
<TouchableOpacity
key={index}
style={[
styles.logEntry,
{ backgroundColor: 'rgba(255,255,255,0.05)' }
]}
onPress={() => copyLog(log)}
activeOpacity={0.7}
>
<View style={styles.logEntryContent}>
<Text style={[
styles.logText,
{
color: isError
? (currentTheme.colors.error || '#ff4444')
: isWarning
? (currentTheme.colors.warning || '#ffaa00')
: currentTheme.colors.mediumEmphasis
}
]}>
{log}
</Text>
<MaterialIcons
name="content-copy"
size={14}
color={currentTheme.colors.mediumEmphasis}
style={styles.logCopyIcon}
/>
</View>
</TouchableOpacity>
);
})
)}
</ScrollView>
</View>
</SettingsCard>
)}
</View>
);
};
const SettingsScreen: React.FC = () => {
const { settings, updateSetting } = useSettings();
@ -927,7 +585,7 @@ const SettingsScreen: React.FC = () => {
/>
<SettingItem
title="Version"
description="0.6.0-beta.8-test"
description="0.6.0-beta.9"
icon="info-outline"
isLast={true}
isTablet={isTablet}
@ -1003,7 +661,19 @@ const SettingsScreen: React.FC = () => {
) : null;
case 'updates':
return <UpdatesSection isTablet={isTablet} />;
return (
<SettingsCard title="UPDATES" isTablet={isTablet}>
<SettingItem
title="App Updates"
description="Check for updates and manage app version"
icon="system-update"
renderControl={ChevronRight}
onPress={() => navigation.navigate('Update')}
isLast={true}
isTablet={isTablet}
/>
</SettingsCard>
);
default:
return null;
@ -1405,62 +1075,6 @@ const styles = StyleSheet.create({
borderTopColor: 'transparent',
marginRight: 8,
},
// Logs styles
logsContainer: {
padding: 16,
},
logsHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
logsHeaderText: {
fontSize: 16,
fontWeight: '600',
},
logsActions: {
flexDirection: 'row',
gap: 8,
},
logActionButton: {
width: 32,
height: 32,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
},
logsScrollView: {
maxHeight: 200,
borderRadius: 8,
padding: 12,
},
logEntry: {
marginBottom: 4,
paddingVertical: 4,
paddingHorizontal: 8,
borderRadius: 4,
},
logEntryContent: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
logText: {
fontSize: 12,
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
lineHeight: 16,
flex: 1,
marginRight: 8,
},
logCopyIcon: {
opacity: 0.6,
},
noLogsText: {
fontSize: 14,
textAlign: 'center',
paddingVertical: 20,
},
});
export default SettingsScreen;

View file

@ -0,0 +1,866 @@
import React, { useState, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
ScrollView,
SafeAreaView,
StatusBar,
Alert,
Platform,
Dimensions,
Clipboard
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
import { MaterialIcons } from '@expo/vector-icons';
import { RootStackParamList } from '../navigation/AppNavigator';
import { useTheme } from '../contexts/ThemeContext';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import UpdateService from '../services/updateService';
const { width, height } = Dimensions.get('window');
const isTablet = width >= 768;
// Card component with minimalistic style
interface SettingsCardProps {
children: React.ReactNode;
title?: string;
isTablet?: boolean;
}
const SettingsCard: React.FC<SettingsCardProps> = ({ children, title, isTablet = false }) => {
const { currentTheme } = useTheme();
return (
<View
style={[
styles.cardContainer,
isTablet && styles.tabletCardContainer
]}
>
{title && (
<Text style={[
styles.cardTitle,
{ color: currentTheme.colors.mediumEmphasis },
isTablet && styles.tabletCardTitle
]}>
{title}
</Text>
)}
<View style={[
styles.card,
{ backgroundColor: currentTheme.colors.elevation1 },
isTablet && styles.tabletCard
]}>
{children}
</View>
</View>
);
};
const UpdateScreen: React.FC = () => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme();
const insets = useSafeAreaInsets();
const [updateInfo, setUpdateInfo] = useState<any>(null);
const [isChecking, setIsChecking] = useState(false);
const [isInstalling, setIsInstalling] = useState(false);
const [lastChecked, setLastChecked] = useState<Date | null>(null);
const [logs, setLogs] = useState<string[]>([]);
const [showLogs, setShowLogs] = useState(false);
const [lastOperation, setLastOperation] = useState<string>('');
const [updateProgress, setUpdateProgress] = useState<number>(0);
const [updateStatus, setUpdateStatus] = useState<'idle' | 'checking' | 'available' | 'downloading' | 'installing' | 'success' | 'error'>('idle');
const checkForUpdates = async () => {
try {
setIsChecking(true);
setUpdateStatus('checking');
setUpdateProgress(0);
setLastOperation('Checking for updates...');
const info = await UpdateService.checkForUpdates();
setUpdateInfo(info);
setLastChecked(new Date());
// Refresh logs after operation
const logs = UpdateService.getLogs();
setLogs(logs);
if (info.isAvailable) {
setUpdateStatus('available');
setLastOperation(`Update available: ${info.manifest?.id?.substring(0, 8) || 'unknown'}...`);
} else {
setUpdateStatus('idle');
setLastOperation('No updates available');
}
} catch (error) {
console.error('Error checking for updates:', error);
setUpdateStatus('error');
setLastOperation(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
Alert.alert('Error', 'Failed to check for updates');
} finally {
setIsChecking(false);
}
};
const installUpdate = async () => {
try {
setIsInstalling(true);
setUpdateStatus('downloading');
setUpdateProgress(0);
setLastOperation('Downloading update...');
// Simulate progress updates
const progressInterval = setInterval(() => {
setUpdateProgress(prev => {
if (prev >= 90) return prev;
return prev + Math.random() * 10;
});
}, 500);
const success = await UpdateService.downloadAndInstallUpdate();
clearInterval(progressInterval);
setUpdateProgress(100);
setUpdateStatus('installing');
setLastOperation('Installing update...');
// Refresh logs after operation
const logs = UpdateService.getLogs();
setLogs(logs);
if (success) {
setUpdateStatus('success');
setLastOperation('Update installed successfully');
Alert.alert('Success', 'Update will be applied on next app restart');
} else {
setUpdateStatus('error');
setLastOperation('No update available to install');
Alert.alert('No Update', 'No update available to install');
}
} catch (error) {
console.error('Error installing update:', error);
setUpdateStatus('error');
setLastOperation(`Installation error: ${error instanceof Error ? error.message : 'Unknown error'}`);
Alert.alert('Error', 'Failed to install update');
} finally {
setIsInstalling(false);
}
};
const getCurrentUpdateInfo = async () => {
const info = await UpdateService.getCurrentUpdateInfo();
setUpdateInfo(info);
const logs = UpdateService.getLogs();
setLogs(logs);
};
const refreshLogs = () => {
const logs = UpdateService.getLogs();
setLogs(logs);
};
const clearLogs = () => {
UpdateService.clearLogs();
setLogs([]);
setLastOperation('Logs cleared');
};
const copyLog = (logText: string) => {
Clipboard.setString(logText);
Alert.alert('Copied', 'Log entry copied to clipboard');
};
const copyAllLogs = () => {
const allLogsText = logs.join('\n');
Clipboard.setString(allLogsText);
Alert.alert('Copied', 'All logs copied to clipboard');
};
const addTestLog = () => {
UpdateService.addTestLog(`Test log entry at ${new Date().toISOString()}`);
const logs = UpdateService.getLogs();
setLogs(logs);
setLastOperation('Test log added');
};
const testConnectivity = async () => {
try {
setLastOperation('Testing connectivity...');
const isReachable = await UpdateService.testUpdateConnectivity();
const logs = UpdateService.getLogs();
setLogs(logs);
if (isReachable) {
setLastOperation('Update server is reachable');
} else {
setLastOperation('Update server is not reachable');
}
} catch (error) {
console.error('Error testing connectivity:', error);
setLastOperation(`Connectivity test error: ${error instanceof Error ? error.message : 'Unknown error'}`);
const logs = UpdateService.getLogs();
setLogs(logs);
}
};
const testAssetUrls = async () => {
try {
setLastOperation('Testing asset URLs...');
await UpdateService.testAllAssetUrls();
const logs = UpdateService.getLogs();
setLogs(logs);
setLastOperation('Asset URL testing completed');
} catch (error) {
console.error('Error testing asset URLs:', error);
setLastOperation(`Asset URL test error: ${error instanceof Error ? error.message : 'Unknown error'}`);
const logs = UpdateService.getLogs();
setLogs(logs);
}
};
// Load current update info on mount
useEffect(() => {
const loadInitialData = async () => {
await getCurrentUpdateInfo();
// Also refresh logs to ensure we have the latest
refreshLogs();
};
loadInitialData();
}, []);
const formatDate = (date: Date) => {
return date.toLocaleString();
};
const getStatusIcon = () => {
switch (updateStatus) {
case 'checking':
return <MaterialIcons name="refresh" size={20} color={currentTheme.colors.primary} />;
case 'available':
return <MaterialIcons name="new-releases" size={20} color={currentTheme.colors.success || '#4CAF50'} />;
case 'downloading':
return <MaterialIcons name="cloud-download" size={20} color={currentTheme.colors.primary} />;
case 'installing':
return <MaterialIcons name="install-mobile" size={20} color={currentTheme.colors.primary} />;
case 'success':
return <MaterialIcons name="check-circle" size={20} color={currentTheme.colors.success || '#4CAF50'} />;
case 'error':
return <MaterialIcons name="error" size={20} color={currentTheme.colors.error || '#ff4444'} />;
default:
return <MaterialIcons name="system-update" size={20} color={currentTheme.colors.mediumEmphasis} />;
}
};
const getStatusText = () => {
switch (updateStatus) {
case 'checking':
return 'Checking for updates...';
case 'available':
return 'Update available!';
case 'downloading':
return 'Downloading update...';
case 'installing':
return 'Installing update...';
case 'success':
return 'Update installed successfully!';
case 'error':
return 'Update failed';
default:
return 'Ready to check for updates';
}
};
const getStatusColor = () => {
switch (updateStatus) {
case 'available':
case 'success':
return currentTheme.colors.success || '#4CAF50';
case 'error':
return currentTheme.colors.error || '#ff4444';
case 'checking':
case 'downloading':
case 'installing':
return currentTheme.colors.primary;
default:
return currentTheme.colors.mediumEmphasis;
}
};
const headerBaseHeight = Platform.OS === 'android' ? 80 : 60;
const topSpacing = Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : insets.top;
const headerHeight = headerBaseHeight + topSpacing;
return (
<View style={[
styles.container,
{ backgroundColor: currentTheme.colors.darkBackground }
]}>
<StatusBar barStyle={'light-content'} />
<View style={{ flex: 1 }}>
<View style={[styles.header, { height: headerHeight, paddingTop: topSpacing }]}>
<TouchableOpacity
style={styles.backButton}
onPress={() => navigation.goBack()}
activeOpacity={0.7}
>
<MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.highEmphasis} />
</TouchableOpacity>
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
App Updates
</Text>
<View style={styles.headerSpacer} />
</View>
<View style={styles.contentContainer}>
<ScrollView
style={styles.scrollView}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.scrollContent}
>
<SettingsCard title="APP UPDATES" isTablet={isTablet}>
{/* Main Update Card */}
<View style={styles.updateMainCard}>
{/* Status Section */}
<View style={styles.updateStatusSection}>
<View style={[styles.statusIndicator, { backgroundColor: `${getStatusColor()}20` }]}>
{getStatusIcon()}
</View>
<View style={styles.statusContent}>
<Text style={[styles.statusMainText, { color: currentTheme.colors.highEmphasis }]}>
{getStatusText()}
</Text>
<Text style={[styles.statusDetailText, { color: currentTheme.colors.mediumEmphasis }]}>
{lastOperation || 'Ready to check for updates'}
</Text>
</View>
</View>
{/* Progress Section */}
{(updateStatus === 'downloading' || updateStatus === 'installing') && (
<View style={styles.progressSection}>
<View style={styles.progressHeader}>
<Text style={[styles.progressLabel, { color: currentTheme.colors.mediumEmphasis }]}>
{updateStatus === 'downloading' ? 'Downloading' : 'Installing'}
</Text>
<Text style={[styles.progressPercentage, { color: currentTheme.colors.primary }]}>
{Math.round(updateProgress)}%
</Text>
</View>
<View style={[styles.modernProgressBar, { backgroundColor: `${currentTheme.colors.primary}15` }]}>
<View
style={[
styles.modernProgressFill,
{
backgroundColor: currentTheme.colors.primary,
width: `${updateProgress}%`
}
]}
/>
</View>
</View>
)}
{/* Action Section */}
<View style={styles.actionSection}>
<TouchableOpacity
style={[
styles.modernButton,
styles.primaryAction,
{ backgroundColor: currentTheme.colors.primary },
(isChecking || isInstalling) && styles.disabledAction
]}
onPress={checkForUpdates}
disabled={isChecking || isInstalling}
activeOpacity={0.8}
>
{isChecking ? (
<MaterialIcons name="refresh" size={18} color="white" />
) : (
<MaterialIcons name="system-update" size={18} color="white" />
)}
<Text style={styles.modernButtonText}>
{isChecking ? 'Checking...' : 'Check for Updates'}
</Text>
</TouchableOpacity>
{updateInfo?.isAvailable && updateStatus !== 'success' && (
<TouchableOpacity
style={[
styles.modernButton,
styles.installAction,
{ backgroundColor: currentTheme.colors.success || '#34C759' },
(isInstalling) && styles.disabledAction
]}
onPress={installUpdate}
disabled={isInstalling}
activeOpacity={0.8}
>
{isInstalling ? (
<MaterialIcons name="install-mobile" size={18} color="white" />
) : (
<MaterialIcons name="download" size={18} color="white" />
)}
<Text style={styles.modernButtonText}>
{isInstalling ? 'Installing...' : 'Install Update'}
</Text>
</TouchableOpacity>
)}
</View>
</View>
{/* Info Section */}
<View style={styles.infoSection}>
<View style={styles.infoItem}>
<View style={[styles.infoIcon, { backgroundColor: `${currentTheme.colors.primary}15` }]}>
<MaterialIcons name="info-outline" size={14} color={currentTheme.colors.primary} />
</View>
<Text style={[styles.infoLabel, { color: currentTheme.colors.mediumEmphasis }]}>Version:</Text>
<Text style={[styles.infoValue, { color: currentTheme.colors.highEmphasis }]}>
{updateInfo?.manifest?.id ? `${updateInfo.manifest.id.substring(0, 8)}...` : 'Unknown'}
</Text>
</View>
{lastChecked && (
<View style={styles.infoItem}>
<View style={[styles.infoIcon, { backgroundColor: `${currentTheme.colors.primary}15` }]}>
<MaterialIcons name="schedule" size={14} color={currentTheme.colors.primary} />
</View>
<Text style={[styles.infoLabel, { color: currentTheme.colors.mediumEmphasis }]}>Last checked:</Text>
<Text style={[styles.infoValue, { color: currentTheme.colors.highEmphasis }]}>
{formatDate(lastChecked)}
</Text>
</View>
)}
</View>
{/* Advanced Toggle */}
<TouchableOpacity
style={[styles.modernAdvancedToggle, { backgroundColor: `${currentTheme.colors.primary}08` }]}
onPress={() => setShowLogs(!showLogs)}
activeOpacity={0.7}
>
<View style={styles.advancedToggleLeft}>
<MaterialIcons name="code" size={18} color={currentTheme.colors.primary} />
<Text style={[styles.advancedToggleLabel, { color: currentTheme.colors.primary }]}>
Developer Logs
</Text>
<View style={[styles.logsBadge, { backgroundColor: currentTheme.colors.primary }]}>
<Text style={styles.logsBadgeText}>{logs.length}</Text>
</View>
</View>
<MaterialIcons
name={showLogs ? "keyboard-arrow-up" : "keyboard-arrow-down"}
size={20}
color={currentTheme.colors.primary}
/>
</TouchableOpacity>
</SettingsCard>
{showLogs && (
<SettingsCard title="UPDATE LOGS" isTablet={isTablet}>
<View style={styles.logsContainer}>
<View style={styles.logsHeader}>
<Text style={[styles.logsHeaderText, { color: currentTheme.colors.highEmphasis }]}>
Update Service Logs
</Text>
<View style={styles.logsActions}>
<TouchableOpacity
style={[styles.logActionButton, { backgroundColor: currentTheme.colors.elevation2 }]}
onPress={testConnectivity}
activeOpacity={0.7}
>
<MaterialIcons name="wifi" size={16} color={currentTheme.colors.primary} />
</TouchableOpacity>
<TouchableOpacity
style={[styles.logActionButton, { backgroundColor: currentTheme.colors.elevation2 }]}
onPress={testAssetUrls}
activeOpacity={0.7}
>
<MaterialIcons name="link" size={16} color={currentTheme.colors.primary} />
</TouchableOpacity>
<TouchableOpacity
style={[styles.logActionButton, { backgroundColor: currentTheme.colors.elevation2 }]}
onPress={addTestLog}
activeOpacity={0.7}
>
<MaterialIcons name="add" size={16} color={currentTheme.colors.primary} />
</TouchableOpacity>
<TouchableOpacity
style={[styles.logActionButton, { backgroundColor: currentTheme.colors.elevation2 }]}
onPress={copyAllLogs}
activeOpacity={0.7}
>
<MaterialIcons name="content-copy" size={16} color={currentTheme.colors.primary} />
</TouchableOpacity>
<TouchableOpacity
style={[styles.logActionButton, { backgroundColor: currentTheme.colors.elevation2 }]}
onPress={refreshLogs}
activeOpacity={0.7}
>
<MaterialIcons name="refresh" size={16} color={currentTheme.colors.primary} />
</TouchableOpacity>
<TouchableOpacity
style={[styles.logActionButton, { backgroundColor: currentTheme.colors.elevation2 }]}
onPress={clearLogs}
activeOpacity={0.7}
>
<MaterialIcons name="clear" size={16} color={currentTheme.colors.error || '#ff4444'} />
</TouchableOpacity>
</View>
</View>
<ScrollView
style={[styles.logsScrollView, { backgroundColor: currentTheme.colors.elevation2 }]}
showsVerticalScrollIndicator={true}
nestedScrollEnabled={true}
>
{logs.length === 0 ? (
<Text style={[styles.noLogsText, { color: currentTheme.colors.mediumEmphasis }]}>
No logs available
</Text>
) : (
logs.map((log, index) => {
const isError = log.includes('[ERROR]');
const isWarning = log.includes('[WARN]');
return (
<TouchableOpacity
key={index}
style={[
styles.logEntry,
{ backgroundColor: 'rgba(255,255,255,0.05)' }
]}
onPress={() => copyLog(log)}
activeOpacity={0.7}
>
<View style={styles.logEntryContent}>
<Text style={[
styles.logText,
{
color: isError
? (currentTheme.colors.error || '#ff4444')
: isWarning
? (currentTheme.colors.warning || '#ffaa00')
: currentTheme.colors.mediumEmphasis
}
]}>
{log}
</Text>
<MaterialIcons
name="content-copy"
size={14}
color={currentTheme.colors.mediumEmphasis}
style={styles.logCopyIcon}
/>
</View>
</TouchableOpacity>
);
})
)}
</ScrollView>
</View>
</SettingsCard>
)}
</ScrollView>
</View>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
paddingHorizontal: Math.max(12, width * 0.04),
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingBottom: 8,
backgroundColor: 'transparent',
zIndex: 2,
},
backButton: {
padding: 8,
marginLeft: -8,
},
headerTitle: {
fontSize: Math.min(24, width * 0.06),
fontWeight: '800',
letterSpacing: 0.3,
flex: 1,
textAlign: 'center',
},
headerSpacer: {
width: 40, // Same width as back button to center the title
},
contentContainer: {
flex: 1,
zIndex: 1,
width: '100%',
},
scrollView: {
flex: 1,
width: '100%',
},
scrollContent: {
flexGrow: 1,
width: '100%',
paddingBottom: 90,
},
// Common card styles
cardContainer: {
width: '100%',
marginBottom: 20,
},
tabletCardContainer: {
marginBottom: 32,
},
cardTitle: {
fontSize: 13,
fontWeight: '600',
letterSpacing: 0.8,
marginLeft: Math.max(12, width * 0.04),
marginBottom: 8,
},
tabletCardTitle: {
fontSize: 14,
marginLeft: 0,
marginBottom: 12,
},
card: {
marginHorizontal: Math.max(12, width * 0.04),
borderRadius: 16,
overflow: 'hidden',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
width: undefined,
},
tabletCard: {
marginHorizontal: 0,
borderRadius: 20,
shadowOpacity: 0.15,
shadowRadius: 8,
elevation: 5,
},
// Update UI Styles
updateMainCard: {
padding: 20,
marginBottom: 16,
},
updateStatusSection: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 20,
},
statusIndicator: {
width: 48,
height: 48,
borderRadius: 24,
alignItems: 'center',
justifyContent: 'center',
marginRight: 16,
},
statusContent: {
flex: 1,
},
statusMainText: {
fontSize: 18,
fontWeight: '600',
marginBottom: 4,
letterSpacing: 0.2,
},
statusDetailText: {
fontSize: 14,
opacity: 0.8,
lineHeight: 20,
},
progressSection: {
marginBottom: 20,
},
progressHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
},
progressLabel: {
fontSize: 14,
fontWeight: '500',
},
progressPercentage: {
fontSize: 14,
fontWeight: '600',
},
modernProgressBar: {
height: 8,
borderRadius: 4,
overflow: 'hidden',
},
modernProgressFill: {
height: '100%',
borderRadius: 4,
},
actionSection: {
gap: 12,
},
modernButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 14,
paddingHorizontal: 20,
borderRadius: 12,
gap: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2,
},
primaryAction: {
marginBottom: 8,
},
installAction: {
// Additional styles for install button
},
disabledAction: {
opacity: 0.6,
},
modernButtonText: {
color: 'white',
fontSize: 15,
fontWeight: '600',
letterSpacing: 0.3,
},
infoSection: {
paddingHorizontal: 20,
paddingVertical: 16,
gap: 12,
},
infoItem: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
infoIcon: {
width: 24,
height: 24,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
},
infoLabel: {
fontSize: 14,
fontWeight: '500',
minWidth: 80,
},
infoValue: {
fontSize: 14,
fontWeight: '400',
flex: 1,
},
modernAdvancedToggle: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 16,
paddingHorizontal: 20,
marginTop: 8,
borderRadius: 12,
marginHorizontal: 4,
},
advancedToggleLeft: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
advancedToggleLabel: {
fontSize: 15,
fontWeight: '500',
letterSpacing: 0.2,
},
logsBadge: {
minWidth: 20,
height: 20,
borderRadius: 10,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 6,
},
logsBadgeText: {
color: 'white',
fontSize: 11,
fontWeight: '600',
},
// Logs styles
logsContainer: {
padding: 20,
},
logsHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
logsHeaderText: {
fontSize: 16,
fontWeight: '600',
},
logsActions: {
flexDirection: 'row',
gap: 8,
},
logActionButton: {
width: 32,
height: 32,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
},
logsScrollView: {
maxHeight: 200,
borderRadius: 8,
padding: 12,
},
logEntry: {
marginBottom: 4,
paddingVertical: 4,
paddingHorizontal: 8,
borderRadius: 4,
},
logEntryContent: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
logText: {
fontSize: 12,
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
lineHeight: 16,
flex: 1,
marginRight: 8,
},
logCopyIcon: {
opacity: 0.6,
},
noLogsText: {
fontSize: 14,
textAlign: 'center',
paddingVertical: 20,
},
});
export default UpdateScreen;

View file

@ -58,9 +58,10 @@ fi
APP_JSON="./app.json"
SETTINGS_SCREEN="./src/screens/SettingsScreen.tsx"
INFO_PLIST="./ios/Nuvio/Info.plist"
ANDROID_BUILD_GRADLE="./android/app/build.gradle"
# Check if files exist
for file in "$APP_JSON" "$SETTINGS_SCREEN" "$INFO_PLIST"; do
for file in "$APP_JSON" "$SETTINGS_SCREEN" "$INFO_PLIST" "$ANDROID_BUILD_GRADLE"; do
if [ ! -f "$file" ]; then
print_error "File not found: $file"
exit 1
@ -89,6 +90,7 @@ print_status "Creating backups..."
cp "$APP_JSON" "${APP_JSON}.backup"
cp "$SETTINGS_SCREEN" "${SETTINGS_SCREEN}.backup"
cp "$INFO_PLIST" "${INFO_PLIST}.backup"
cp "$ANDROID_BUILD_GRADLE" "${ANDROID_BUILD_GRADLE}.backup"
# Function to restore backups on error
restore_backups() {
@ -96,6 +98,7 @@ restore_backups() {
mv "${APP_JSON}.backup" "$APP_JSON"
mv "${SETTINGS_SCREEN}.backup" "$SETTINGS_SCREEN"
mv "${INFO_PLIST}.backup" "$INFO_PLIST"
mv "${ANDROID_BUILD_GRADLE}.backup" "$ANDROID_BUILD_GRADLE"
}
# Set trap to restore backups on error
@ -105,6 +108,8 @@ trap restore_backups ERR
print_status "Updating app.json..."
# Update version in expo section
sed -i '' "s/\"version\": \"[^\"]*\"/\"version\": \"$NEW_VERSION\"/g" "$APP_JSON"
# Update ALL runtimeVersion fields (handles multiple instances if they exist)
sed -i '' "s/\"runtimeVersion\": \"[^\"]*\"/\"runtimeVersion\": \"$NEW_VERSION\"/g" "$APP_JSON"
# Update versionCode in android section
sed -i '' "s/\"versionCode\": [0-9]*/\"versionCode\": $NEW_BUILD_NUMBER/g" "$APP_JSON"
# Update buildNumber in ios section
@ -125,11 +130,20 @@ sed -i '' "/<key>CFBundleShortVersionString<\/key>/{n;s/<string>[^<]*<\/string>/
sed -i '' "/<key>CFBundleVersion<\/key>/{n;s/<string>[^<]*<\/string>/<string>$NEW_BUILD_NUMBER<\/string>/;}" "$INFO_PLIST"
print_success "Updated Info.plist"
# Update Android build.gradle
print_status "Updating Android build.gradle..."
# Update versionCode
sed -i '' "s/versionCode [0-9]*/versionCode $NEW_BUILD_NUMBER/g" "$ANDROID_BUILD_GRADLE"
# Update versionName
sed -i '' "s/versionName \"[^\"]*\"/versionName \"$NEW_VERSION\"/g" "$ANDROID_BUILD_GRADLE"
print_success "Updated Android build.gradle"
# Verify updates
print_status "Verifying updates..."
# Check app.json
if grep -q "\"version\": \"$NEW_VERSION\"" "$APP_JSON" &&
grep -q "\"runtimeVersion\": \"$NEW_VERSION\"" "$APP_JSON" &&
grep -q "\"versionCode\": $NEW_BUILD_NUMBER" "$APP_JSON" &&
grep -q "\"buildNumber\": \"$NEW_BUILD_NUMBER\"" "$APP_JSON"; then
print_success "app.json updated correctly"
@ -155,15 +169,25 @@ else
exit 1
fi
# Check Android build.gradle
if grep -q "versionCode $NEW_BUILD_NUMBER" "$ANDROID_BUILD_GRADLE" &&
grep -q "versionName \"$NEW_VERSION\"" "$ANDROID_BUILD_GRADLE"; then
print_success "Android build.gradle updated correctly"
else
print_error "Android build.gradle update verification failed"
exit 1
fi
# Clean up backups
print_status "Cleaning up backups..."
rm "${APP_JSON}.backup" "${SETTINGS_SCREEN}.backup" "${INFO_PLIST}.backup"
rm "${APP_JSON}.backup" "${SETTINGS_SCREEN}.backup" "${INFO_PLIST}.backup" "${ANDROID_BUILD_GRADLE}.backup"
print_success "Version update completed successfully!"
print_status "Summary:"
echo " Version: $NEW_VERSION"
echo " Runtime Version: $NEW_VERSION"
echo " Build Number: $NEW_BUILD_NUMBER"
echo " Files updated: app.json, SettingsScreen.tsx, Info.plist"
echo " Files updated: app.json, SettingsScreen.tsx, Info.plist, Android build.gradle"
echo ""
print_status "Next steps:"
echo " 1. Test the app to ensure everything works correctly"