mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 12:00:33 +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 { 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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
12
app.json
12
app.json
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
13
index.ts
13
index.ts
|
|
@ -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
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": {
|
||||
"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",
|
||||
|
|
|
|||
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
|
||||
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
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 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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
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"
|
||||
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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue