mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
major additions
This commit is contained in:
parent
e4bc0d3896
commit
bf0fe2d5a1
14 changed files with 2633 additions and 1729 deletions
22
App.tsx
22
App.tsx
|
|
@ -27,6 +27,8 @@ import { TraktProvider } from './src/contexts/TraktContext';
|
||||||
import { ThemeProvider, useTheme } from './src/contexts/ThemeContext';
|
import { ThemeProvider, useTheme } from './src/contexts/ThemeContext';
|
||||||
import { TrailerProvider } from './src/contexts/TrailerContext';
|
import { TrailerProvider } from './src/contexts/TrailerContext';
|
||||||
import SplashScreen from './src/components/SplashScreen';
|
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 AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import * as Sentry from '@sentry/react-native';
|
import * as Sentry from '@sentry/react-native';
|
||||||
import UpdateService from './src/services/updateService';
|
import UpdateService from './src/services/updateService';
|
||||||
|
|
@ -61,6 +63,16 @@ const ThemedApp = () => {
|
||||||
const [isAppReady, setIsAppReady] = useState(false);
|
const [isAppReady, setIsAppReady] = useState(false);
|
||||||
const [hasCompletedOnboarding, setHasCompletedOnboarding] = useState<boolean | null>(null);
|
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
|
// Check onboarding status and initialize update service
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initializeApp = async () => {
|
const initializeApp = async () => {
|
||||||
|
|
@ -122,6 +134,16 @@ const ThemedApp = () => {
|
||||||
/>
|
/>
|
||||||
{!isAppReady && <SplashScreen onFinish={handleSplashComplete} />}
|
{!isAppReady && <SplashScreen onFinish={handleSplashComplete} />}
|
||||||
{shouldShowApp && <AppNavigator initialRouteName={initialRouteName} />}
|
{shouldShowApp && <AppNavigator initialRouteName={initialRouteName} />}
|
||||||
|
|
||||||
|
{/* Update Popup */}
|
||||||
|
<UpdatePopup
|
||||||
|
visible={showUpdatePopup}
|
||||||
|
updateInfo={updateInfo}
|
||||||
|
onUpdateNow={handleUpdateNow}
|
||||||
|
onUpdateLater={handleUpdateLater}
|
||||||
|
onDismiss={handleDismiss}
|
||||||
|
isInstalling={isInstalling}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
</PaperProvider>
|
</PaperProvider>
|
||||||
|
|
|
||||||
|
|
@ -93,8 +93,8 @@ android {
|
||||||
applicationId 'com.nuvio.app'
|
applicationId 'com.nuvio.app'
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 8
|
versionCode 9
|
||||||
versionName "0.6.0-beta.8"
|
versionName "0.6.0-beta.9"
|
||||||
}
|
}
|
||||||
|
|
||||||
splits {
|
splits {
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,5 @@
|
||||||
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
|
<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_splash_screen_status_bar_translucent" translatable="false">false</string>
|
||||||
<string name="expo_system_ui_user_interface_style" translatable="false">dark</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>
|
</resources>
|
||||||
12
app.json
12
app.json
|
|
@ -2,7 +2,7 @@
|
||||||
"expo": {
|
"expo": {
|
||||||
"name": "Nuvio",
|
"name": "Nuvio",
|
||||||
"slug": "nuvio",
|
"slug": "nuvio",
|
||||||
"version": "0.6.0-beta.8",
|
"version": "0.6.0-beta.9",
|
||||||
"orientation": "default",
|
"orientation": "default",
|
||||||
"icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png",
|
"icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png",
|
||||||
"userInterfaceStyle": "dark",
|
"userInterfaceStyle": "dark",
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": true,
|
"supportsTablet": true,
|
||||||
"icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png",
|
"icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png",
|
||||||
"buildNumber": "8",
|
"buildNumber": "9",
|
||||||
"infoPlist": {
|
"infoPlist": {
|
||||||
"NSAppTransportSecurity": {
|
"NSAppTransportSecurity": {
|
||||||
"NSAllowsArbitraryLoads": true
|
"NSAllowsArbitraryLoads": true
|
||||||
|
|
@ -46,7 +46,7 @@
|
||||||
"WAKE_LOCK"
|
"WAKE_LOCK"
|
||||||
],
|
],
|
||||||
"package": "com.nuvio.app",
|
"package": "com.nuvio.app",
|
||||||
"versionCode": 8,
|
"versionCode": 9,
|
||||||
"architectures": [
|
"architectures": [
|
||||||
"arm64-v8a",
|
"arm64-v8a",
|
||||||
"armeabi-v7a",
|
"armeabi-v7a",
|
||||||
|
|
@ -54,9 +54,7 @@
|
||||||
"x86_64"
|
"x86_64"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"web": {
|
|
||||||
"favicon": "./assets/favicon.png"
|
|
||||||
},
|
|
||||||
"extra": {
|
"extra": {
|
||||||
"eas": {
|
"eas": {
|
||||||
"projectId": "909107b8-fe61-45ce-b02f-b02510d306a6"
|
"projectId": "909107b8-fe61-45ce-b02f-b02510d306a6"
|
||||||
|
|
@ -86,6 +84,6 @@
|
||||||
"fallbackToCacheTimeout": 0,
|
"fallbackToCacheTimeout": 0,
|
||||||
"url": "https://grim-reyna-tapframe-69970143.koyeb.app/api/manifest"
|
"url": "https://grim-reyna-tapframe-69970143.koyeb.app/api/manifest"
|
||||||
},
|
},
|
||||||
"runtimeVersion": "0.6.0-beta.8"
|
"runtimeVersion": "0.6.0-beta.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
index.ts
13
index.ts
|
|
@ -1,5 +1,18 @@
|
||||||
import { registerRootComponent } from 'expo';
|
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';
|
import App from './App';
|
||||||
|
|
||||||
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
|
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
|
||||||
|
|
|
||||||
2404
package-lock.json
generated
2404
package-lock.json
generated
File diff suppressed because it is too large
Load diff
10
package.json
10
package.json
|
|
@ -5,8 +5,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "expo start",
|
"start": "expo start",
|
||||||
"android": "expo run:android",
|
"android": "expo run:android",
|
||||||
"ios": "expo run:ios",
|
"ios": "expo run:ios"
|
||||||
"web": "expo start --web"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@backpackapp-io/react-native-toast": "^0.14.0",
|
"@backpackapp-io/react-native-toast": "^0.14.0",
|
||||||
|
|
@ -34,8 +33,8 @@
|
||||||
"eventemitter3": "^5.0.1",
|
"eventemitter3": "^5.0.1",
|
||||||
"expo": "~52.0.47",
|
"expo": "~52.0.47",
|
||||||
"expo-application": "~6.0.2",
|
"expo-application": "~6.0.2",
|
||||||
"expo-auth-session": "^6.0.3",
|
"expo-auth-session": "~6.0.3",
|
||||||
"expo-blur": "^14.0.3",
|
"expo-blur": "~14.0.3",
|
||||||
"expo-dev-client": "~5.0.20",
|
"expo-dev-client": "~5.0.20",
|
||||||
"expo-device": "~7.0.3",
|
"expo-device": "~7.0.3",
|
||||||
"expo-file-system": "~18.0.12",
|
"expo-file-system": "~18.0.12",
|
||||||
|
|
@ -68,7 +67,8 @@
|
||||||
"react-native-url-polyfill": "^2.0.0",
|
"react-native-url-polyfill": "^2.0.0",
|
||||||
"react-native-video": "^6.12.0",
|
"react-native-video": "^6.12.0",
|
||||||
"react-native-vlc-media-player": "^1.0.87",
|
"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": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
|
|
|
||||||
361
src/components/UpdatePopup.tsx
Normal file
361
src/components/UpdatePopup.tsx
Normal 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;
|
||||||
|
|
@ -758,9 +758,9 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
||||||
// Ensure trailer state is properly synchronized when trailer becomes ready
|
// Ensure trailer state is properly synchronized when trailer becomes ready
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (trailerReady && settings?.showTrailers && !globalTrailerPlaying) {
|
if (trailerReady && settings?.showTrailers && !globalTrailerPlaying) {
|
||||||
// If trailer is ready but not playing, start it
|
// Only start trailer if it's the initial load, not when returning from other screens
|
||||||
logger.info('HeroSection', 'Starting trailer after it became ready');
|
// This prevents auto-starting when returning from StreamsScreen
|
||||||
setTrailerPlaying(true);
|
logger.info('HeroSection', 'Trailer ready but not playing - not auto-starting to prevent unwanted playback');
|
||||||
}
|
}
|
||||||
}, [trailerReady, settings?.showTrailers, globalTrailerPlaying, setTrailerPlaying]);
|
}, [trailerReady, settings?.showTrailers, globalTrailerPlaying, setTrailerPlaying]);
|
||||||
|
|
||||||
|
|
@ -1033,29 +1033,23 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
||||||
return () => subscription?.remove();
|
return () => subscription?.remove();
|
||||||
}, [setTrailerPlaying, globalTrailerPlaying]);
|
}, [setTrailerPlaying, globalTrailerPlaying]);
|
||||||
|
|
||||||
// Navigation focus effect - improved to prevent trailer interruption
|
// Navigation focus effect - conservative approach to prevent unwanted trailer resumption
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
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');
|
logger.info('HeroSection', 'Screen focused');
|
||||||
|
|
||||||
// Small delay to ensure the screen is fully focused before checking trailer state
|
// Don't automatically resume trailer when returning from other screens
|
||||||
const focusTimer = setTimeout(() => {
|
// This prevents the trailer from starting when returning from StreamsScreen
|
||||||
// If trailer should be playing but isn't, resume it
|
// The trailer should only resume if the user explicitly wants it to play
|
||||||
if (settings?.showTrailers && trailerReady && !globalTrailerPlaying) {
|
|
||||||
logger.info('HeroSection', 'Resuming trailer after screen focus');
|
|
||||||
setTrailerPlaying(true);
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearTimeout(focusTimer);
|
|
||||||
// Don't automatically stop trailer when component unmounts
|
// Don't automatically stop trailer when component unmounts
|
||||||
// Let the new hero section (if any) take control of trailer state
|
// Let the new hero section (if any) take control of trailer state
|
||||||
// This prevents the trailer from stopping when navigating between screens
|
// This prevents the trailer from stopping when navigating between screens
|
||||||
logger.info('HeroSection', 'Screen unfocused - not stopping trailer automatically');
|
logger.info('HeroSection', 'Screen unfocused - not stopping trailer automatically');
|
||||||
};
|
};
|
||||||
}, [settings?.showTrailers, trailerReady, globalTrailerPlaying, setTrailerPlaying])
|
}, [])
|
||||||
);
|
);
|
||||||
|
|
||||||
// Memory management and cleanup
|
// Memory management and cleanup
|
||||||
|
|
|
||||||
161
src/hooks/useUpdatePopup.ts
Normal file
161
src/hooks/useUpdatePopup.ts
Normal 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,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -47,6 +47,7 @@ import { AccountProvider, useAccount } from '../contexts/AccountContext';
|
||||||
import { LoadingProvider, useLoading } from '../contexts/LoadingContext';
|
import { LoadingProvider, useLoading } from '../contexts/LoadingContext';
|
||||||
import PluginsScreen from '../screens/PluginsScreen';
|
import PluginsScreen from '../screens/PluginsScreen';
|
||||||
import CastMoviesScreen from '../screens/CastMoviesScreen';
|
import CastMoviesScreen from '../screens/CastMoviesScreen';
|
||||||
|
import UpdateScreen from '../screens/UpdateScreen';
|
||||||
|
|
||||||
// Stack navigator types
|
// Stack navigator types
|
||||||
export type RootStackParamList = {
|
export type RootStackParamList = {
|
||||||
|
|
@ -55,6 +56,7 @@ export type RootStackParamList = {
|
||||||
Home: undefined;
|
Home: undefined;
|
||||||
Library: undefined;
|
Library: undefined;
|
||||||
Settings: undefined;
|
Settings: undefined;
|
||||||
|
Update: undefined;
|
||||||
Search: undefined;
|
Search: undefined;
|
||||||
Calendar: undefined;
|
Calendar: undefined;
|
||||||
Metadata: {
|
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>
|
</Stack.Navigator>
|
||||||
</View>
|
</View>
|
||||||
</PaperProvider>
|
</PaperProvider>
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import {
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { NavigationProp } 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 { Picker } from '@react-native-picker/picker';
|
||||||
import { useSettings, DEFAULT_SETTINGS } from '../hooks/useSettings';
|
import { useSettings, DEFAULT_SETTINGS } from '../hooks/useSettings';
|
||||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||||
|
|
@ -31,7 +31,6 @@ import { useAccount } from '../contexts/AccountContext';
|
||||||
import { catalogService } from '../services/catalogService';
|
import { catalogService } from '../services/catalogService';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import * as Sentry from '@sentry/react-native';
|
import * as Sentry from '@sentry/react-native';
|
||||||
import UpdateService from '../services/updateService';
|
|
||||||
|
|
||||||
const { width, height } = Dimensions.get('window');
|
const { width, height } = Dimensions.get('window');
|
||||||
const isTablet = width >= 768;
|
const isTablet = width >= 768;
|
||||||
|
|
@ -40,15 +39,15 @@ const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
||||||
|
|
||||||
// Settings categories for tablet sidebar
|
// Settings categories for tablet sidebar
|
||||||
const SETTINGS_CATEGORIES = [
|
const SETTINGS_CATEGORIES = [
|
||||||
{ id: 'account', title: 'Account', icon: 'account-circle' },
|
{ id: 'account', title: 'Account', icon: 'account-circle' as keyof typeof MaterialIcons.glyphMap },
|
||||||
{ id: 'content', title: 'Content & Discovery', icon: 'explore' },
|
{ id: 'content', title: 'Content & Discovery', icon: 'explore' as keyof typeof MaterialIcons.glyphMap },
|
||||||
{ id: 'appearance', title: 'Appearance', icon: 'palette' },
|
{ id: 'appearance', title: 'Appearance', icon: 'palette' as keyof typeof MaterialIcons.glyphMap },
|
||||||
{ id: 'integrations', title: 'Integrations', icon: 'extension' },
|
{ id: 'integrations', title: 'Integrations', icon: 'extension' as keyof typeof MaterialIcons.glyphMap },
|
||||||
{ id: 'playback', title: 'Playback', icon: 'play-circle-outline' },
|
{ id: 'playback', title: 'Playback', icon: 'play-circle-outline' as keyof typeof MaterialIcons.glyphMap },
|
||||||
{ id: 'updates', title: 'Updates', icon: 'system-update' },
|
{ id: 'updates', title: 'Updates', icon: 'system-update' as keyof typeof MaterialIcons.glyphMap },
|
||||||
{ id: 'about', title: 'About', icon: 'info-outline' },
|
{ id: 'about', title: 'About', icon: 'info-outline' as keyof typeof MaterialIcons.glyphMap },
|
||||||
{ id: 'developer', title: 'Developer', icon: 'code' },
|
{ id: 'developer', title: 'Developer', icon: 'code' as keyof typeof MaterialIcons.glyphMap },
|
||||||
{ id: 'cache', title: 'Cache', icon: 'cached' },
|
{ id: 'cache', title: 'Cache', icon: 'cached' as keyof typeof MaterialIcons.glyphMap },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Card component with minimalistic style
|
// Card component with minimalistic style
|
||||||
|
|
@ -91,7 +90,7 @@ const SettingsCard: React.FC<SettingsCardProps> = ({ children, title, isTablet =
|
||||||
interface SettingItemProps {
|
interface SettingItemProps {
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
icon: string;
|
icon: keyof typeof MaterialIcons.glyphMap;
|
||||||
renderControl?: () => React.ReactNode;
|
renderControl?: () => React.ReactNode;
|
||||||
isLast?: boolean;
|
isLast?: boolean;
|
||||||
onPress?: () => void;
|
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 SettingsScreen: React.FC = () => {
|
||||||
const { settings, updateSetting } = useSettings();
|
const { settings, updateSetting } = useSettings();
|
||||||
|
|
@ -927,7 +585,7 @@ const SettingsScreen: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
<SettingItem
|
<SettingItem
|
||||||
title="Version"
|
title="Version"
|
||||||
description="0.6.0-beta.8-test"
|
description="0.6.0-beta.9"
|
||||||
icon="info-outline"
|
icon="info-outline"
|
||||||
isLast={true}
|
isLast={true}
|
||||||
isTablet={isTablet}
|
isTablet={isTablet}
|
||||||
|
|
@ -1003,7 +661,19 @@ const SettingsScreen: React.FC = () => {
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
case 'updates':
|
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:
|
default:
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -1405,62 +1075,6 @@ const styles = StyleSheet.create({
|
||||||
borderTopColor: 'transparent',
|
borderTopColor: 'transparent',
|
||||||
marginRight: 8,
|
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;
|
export default SettingsScreen;
|
||||||
866
src/screens/UpdateScreen.tsx
Normal file
866
src/screens/UpdateScreen.tsx
Normal 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;
|
||||||
|
|
@ -58,9 +58,10 @@ fi
|
||||||
APP_JSON="./app.json"
|
APP_JSON="./app.json"
|
||||||
SETTINGS_SCREEN="./src/screens/SettingsScreen.tsx"
|
SETTINGS_SCREEN="./src/screens/SettingsScreen.tsx"
|
||||||
INFO_PLIST="./ios/Nuvio/Info.plist"
|
INFO_PLIST="./ios/Nuvio/Info.plist"
|
||||||
|
ANDROID_BUILD_GRADLE="./android/app/build.gradle"
|
||||||
|
|
||||||
# Check if files exist
|
# 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
|
if [ ! -f "$file" ]; then
|
||||||
print_error "File not found: $file"
|
print_error "File not found: $file"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
@ -89,6 +90,7 @@ print_status "Creating backups..."
|
||||||
cp "$APP_JSON" "${APP_JSON}.backup"
|
cp "$APP_JSON" "${APP_JSON}.backup"
|
||||||
cp "$SETTINGS_SCREEN" "${SETTINGS_SCREEN}.backup"
|
cp "$SETTINGS_SCREEN" "${SETTINGS_SCREEN}.backup"
|
||||||
cp "$INFO_PLIST" "${INFO_PLIST}.backup"
|
cp "$INFO_PLIST" "${INFO_PLIST}.backup"
|
||||||
|
cp "$ANDROID_BUILD_GRADLE" "${ANDROID_BUILD_GRADLE}.backup"
|
||||||
|
|
||||||
# Function to restore backups on error
|
# Function to restore backups on error
|
||||||
restore_backups() {
|
restore_backups() {
|
||||||
|
|
@ -96,6 +98,7 @@ restore_backups() {
|
||||||
mv "${APP_JSON}.backup" "$APP_JSON"
|
mv "${APP_JSON}.backup" "$APP_JSON"
|
||||||
mv "${SETTINGS_SCREEN}.backup" "$SETTINGS_SCREEN"
|
mv "${SETTINGS_SCREEN}.backup" "$SETTINGS_SCREEN"
|
||||||
mv "${INFO_PLIST}.backup" "$INFO_PLIST"
|
mv "${INFO_PLIST}.backup" "$INFO_PLIST"
|
||||||
|
mv "${ANDROID_BUILD_GRADLE}.backup" "$ANDROID_BUILD_GRADLE"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Set trap to restore backups on error
|
# Set trap to restore backups on error
|
||||||
|
|
@ -105,6 +108,8 @@ trap restore_backups ERR
|
||||||
print_status "Updating app.json..."
|
print_status "Updating app.json..."
|
||||||
# Update version in expo section
|
# Update version in expo section
|
||||||
sed -i '' "s/\"version\": \"[^\"]*\"/\"version\": \"$NEW_VERSION\"/g" "$APP_JSON"
|
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
|
# Update versionCode in android section
|
||||||
sed -i '' "s/\"versionCode\": [0-9]*/\"versionCode\": $NEW_BUILD_NUMBER/g" "$APP_JSON"
|
sed -i '' "s/\"versionCode\": [0-9]*/\"versionCode\": $NEW_BUILD_NUMBER/g" "$APP_JSON"
|
||||||
# Update buildNumber in ios section
|
# 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"
|
sed -i '' "/<key>CFBundleVersion<\/key>/{n;s/<string>[^<]*<\/string>/<string>$NEW_BUILD_NUMBER<\/string>/;}" "$INFO_PLIST"
|
||||||
print_success "Updated 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
|
# Verify updates
|
||||||
print_status "Verifying updates..."
|
print_status "Verifying updates..."
|
||||||
|
|
||||||
# Check app.json
|
# Check app.json
|
||||||
if grep -q "\"version\": \"$NEW_VERSION\"" "$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 "\"versionCode\": $NEW_BUILD_NUMBER" "$APP_JSON" &&
|
||||||
grep -q "\"buildNumber\": \"$NEW_BUILD_NUMBER\"" "$APP_JSON"; then
|
grep -q "\"buildNumber\": \"$NEW_BUILD_NUMBER\"" "$APP_JSON"; then
|
||||||
print_success "app.json updated correctly"
|
print_success "app.json updated correctly"
|
||||||
|
|
@ -155,15 +169,25 @@ else
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
# Clean up backups
|
||||||
print_status "Cleaning 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_success "Version update completed successfully!"
|
||||||
print_status "Summary:"
|
print_status "Summary:"
|
||||||
echo " Version: $NEW_VERSION"
|
echo " Version: $NEW_VERSION"
|
||||||
|
echo " Runtime Version: $NEW_VERSION"
|
||||||
echo " Build Number: $NEW_BUILD_NUMBER"
|
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 ""
|
echo ""
|
||||||
print_status "Next steps:"
|
print_status "Next steps:"
|
||||||
echo " 1. Test the app to ensure everything works correctly"
|
echo " 1. Test the app to ensure everything works correctly"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue