a
This commit is contained in:
6
app.json
6
app.json
@@ -17,7 +17,11 @@
|
|||||||
"backgroundImage": "./assets/images/android-icon-background.png",
|
"backgroundImage": "./assets/images/android-icon-background.png",
|
||||||
"monochromeImage": "./assets/images/android-icon-monochrome.png"
|
"monochromeImage": "./assets/images/android-icon-monochrome.png"
|
||||||
},
|
},
|
||||||
"predictiveBackGestureEnabled": false
|
"predictiveBackGestureEnabled": false,
|
||||||
|
"package": "com.nurmuhammet.ali.ugrat",
|
||||||
|
"permissions": [
|
||||||
|
"SEND_SMS"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"output": "static",
|
"output": "static",
|
||||||
|
|||||||
8201
package-lock.json
generated
Normal file
8201
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@@ -4,12 +4,15 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/ui": "~56.0.15",
|
"@expo/ui": "~56.0.15",
|
||||||
|
"@react-native-async-storage/async-storage": "2.2.0",
|
||||||
"expo": "~56.0.8",
|
"expo": "~56.0.8",
|
||||||
"expo-constants": "~56.0.16",
|
"expo-constants": "~56.0.16",
|
||||||
|
"expo-dev-client": "~56.0.18",
|
||||||
"expo-device": "~56.0.4",
|
"expo-device": "~56.0.4",
|
||||||
"expo-font": "~56.0.5",
|
"expo-font": "~56.0.5",
|
||||||
"expo-glass-effect": "~56.0.4",
|
"expo-glass-effect": "~56.0.4",
|
||||||
"expo-image": "~56.0.9",
|
"expo-image": "~56.0.9",
|
||||||
|
"expo-keep-awake": "~56.0.3",
|
||||||
"expo-linking": "~56.0.13",
|
"expo-linking": "~56.0.13",
|
||||||
"expo-router": "~56.2.8",
|
"expo-router": "~56.2.8",
|
||||||
"expo-splash-screen": "~56.0.10",
|
"expo-splash-screen": "~56.0.10",
|
||||||
@@ -24,8 +27,10 @@
|
|||||||
"react-native-reanimated": "4.3.1",
|
"react-native-reanimated": "4.3.1",
|
||||||
"react-native-safe-area-context": "~5.7.0",
|
"react-native-safe-area-context": "~5.7.0",
|
||||||
"react-native-screens": "4.25.2",
|
"react-native-screens": "4.25.2",
|
||||||
|
"react-native-send-direct-sms": "^0.2.0",
|
||||||
"react-native-web": "~0.21.0",
|
"react-native-web": "~0.21.0",
|
||||||
"react-native-worklets": "0.8.3"
|
"react-native-worklets": "0.8.3",
|
||||||
|
"socket.io-client": "^4.8.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "~19.2.2",
|
"@types/react": "~19.2.2",
|
||||||
@@ -34,8 +39,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "expo start",
|
"start": "expo start",
|
||||||
"reset-project": "node ./scripts/reset-project.js",
|
"reset-project": "node ./scripts/reset-project.js",
|
||||||
"android": "expo start --android",
|
"android": "expo run:android",
|
||||||
"ios": "expo start --ios",
|
"ios": "expo run:ios",
|
||||||
"web": "expo start --web",
|
"web": "expo start --web",
|
||||||
"lint": "expo lint"
|
"lint": "expo lint"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,180 +0,0 @@
|
|||||||
import { Image } from 'expo-image';
|
|
||||||
import { SymbolView } from 'expo-symbols';
|
|
||||||
import { Platform, Pressable, ScrollView, StyleSheet } from 'react-native';
|
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
||||||
|
|
||||||
import { ExternalLink } from '@/components/external-link';
|
|
||||||
import { ThemedText } from '@/components/themed-text';
|
|
||||||
import { ThemedView } from '@/components/themed-view';
|
|
||||||
import { Collapsible } from '@/components/ui/collapsible';
|
|
||||||
import { WebBadge } from '@/components/web-badge';
|
|
||||||
import { BottomTabInset, MaxContentWidth, Spacing } from '@/constants/theme';
|
|
||||||
import { useTheme } from '@/hooks/use-theme';
|
|
||||||
|
|
||||||
export default function TabTwoScreen() {
|
|
||||||
const safeAreaInsets = useSafeAreaInsets();
|
|
||||||
const insets = {
|
|
||||||
...safeAreaInsets,
|
|
||||||
bottom: safeAreaInsets.bottom + BottomTabInset + Spacing.three,
|
|
||||||
};
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const contentPlatformStyle = Platform.select({
|
|
||||||
android: {
|
|
||||||
paddingTop: insets.top,
|
|
||||||
paddingLeft: insets.left,
|
|
||||||
paddingRight: insets.right,
|
|
||||||
paddingBottom: insets.bottom,
|
|
||||||
},
|
|
||||||
web: {
|
|
||||||
paddingTop: Spacing.six,
|
|
||||||
paddingBottom: Spacing.four,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollView
|
|
||||||
style={[styles.scrollView, { backgroundColor: theme.background }]}
|
|
||||||
contentInset={insets}
|
|
||||||
contentContainerStyle={[styles.contentContainer, contentPlatformStyle]}>
|
|
||||||
<ThemedView style={styles.container}>
|
|
||||||
<ThemedView style={styles.titleContainer}>
|
|
||||||
<ThemedText type="subtitle">Explore</ThemedText>
|
|
||||||
<ThemedText style={styles.centerText} themeColor="textSecondary">
|
|
||||||
This starter app includes example{'\n'}code to help you get started.
|
|
||||||
</ThemedText>
|
|
||||||
|
|
||||||
<ExternalLink href="https://docs.expo.dev" asChild>
|
|
||||||
<Pressable style={({ pressed }) => pressed && styles.pressed}>
|
|
||||||
<ThemedView type="backgroundElement" style={styles.linkButton}>
|
|
||||||
<ThemedText type="link">Expo documentation</ThemedText>
|
|
||||||
<SymbolView
|
|
||||||
tintColor={theme.text}
|
|
||||||
name={{ ios: 'arrow.up.right.square', android: 'link', web: 'link' }}
|
|
||||||
size={12}
|
|
||||||
/>
|
|
||||||
</ThemedView>
|
|
||||||
</Pressable>
|
|
||||||
</ExternalLink>
|
|
||||||
</ThemedView>
|
|
||||||
|
|
||||||
<ThemedView style={styles.sectionsWrapper}>
|
|
||||||
<Collapsible title="File-based routing">
|
|
||||||
<ThemedText type="small">
|
|
||||||
This app has two screens: <ThemedText type="code">src/app/index.tsx</ThemedText> and{' '}
|
|
||||||
<ThemedText type="code">src/app/explore.tsx</ThemedText>
|
|
||||||
</ThemedText>
|
|
||||||
<ThemedText type="small">
|
|
||||||
The layout file in <ThemedText type="code">src/app/_layout.tsx</ThemedText> sets up
|
|
||||||
the tab navigator.
|
|
||||||
</ThemedText>
|
|
||||||
<ExternalLink href="https://docs.expo.dev/router/introduction">
|
|
||||||
<ThemedText type="linkPrimary">Learn more</ThemedText>
|
|
||||||
</ExternalLink>
|
|
||||||
</Collapsible>
|
|
||||||
|
|
||||||
<Collapsible title="Android, iOS, and web support">
|
|
||||||
<ThemedView type="backgroundElement" style={styles.collapsibleContent}>
|
|
||||||
<ThemedText type="small">
|
|
||||||
You can open this project on Android, iOS, and the web. To open the web version,
|
|
||||||
press <ThemedText type="smallBold">w</ThemedText> in the terminal running this
|
|
||||||
project.
|
|
||||||
</ThemedText>
|
|
||||||
<Image
|
|
||||||
source={require('@/assets/images/tutorial-web.png')}
|
|
||||||
style={styles.imageTutorial}
|
|
||||||
/>
|
|
||||||
</ThemedView>
|
|
||||||
</Collapsible>
|
|
||||||
|
|
||||||
<Collapsible title="Images">
|
|
||||||
<ThemedText type="small">
|
|
||||||
For static images, you can use the <ThemedText type="code">@2x</ThemedText> and{' '}
|
|
||||||
<ThemedText type="code">@3x</ThemedText> suffixes to provide files for different
|
|
||||||
screen densities.
|
|
||||||
</ThemedText>
|
|
||||||
<Image source={require('@/assets/images/react-logo.png')} style={styles.imageReact} />
|
|
||||||
<ExternalLink href="https://reactnative.dev/docs/images">
|
|
||||||
<ThemedText type="linkPrimary">Learn more</ThemedText>
|
|
||||||
</ExternalLink>
|
|
||||||
</Collapsible>
|
|
||||||
|
|
||||||
<Collapsible title="Light and dark mode components">
|
|
||||||
<ThemedText type="small">
|
|
||||||
This template has light and dark mode support. The{' '}
|
|
||||||
<ThemedText type="code">useColorScheme()</ThemedText> hook lets you inspect what the
|
|
||||||
user's current color scheme is, and so you can adjust UI colors accordingly.
|
|
||||||
</ThemedText>
|
|
||||||
<ExternalLink href="https://docs.expo.dev/develop/user-interface/color-themes/">
|
|
||||||
<ThemedText type="linkPrimary">Learn more</ThemedText>
|
|
||||||
</ExternalLink>
|
|
||||||
</Collapsible>
|
|
||||||
|
|
||||||
<Collapsible title="Animations">
|
|
||||||
<ThemedText type="small">
|
|
||||||
This template includes an example of an animated component. The{' '}
|
|
||||||
<ThemedText type="code">src/components/ui/collapsible.tsx</ThemedText> component uses
|
|
||||||
the powerful <ThemedText type="code">react-native-reanimated</ThemedText> library to
|
|
||||||
animate opening this hint.
|
|
||||||
</ThemedText>
|
|
||||||
</Collapsible>
|
|
||||||
</ThemedView>
|
|
||||||
{Platform.OS === 'web' && <WebBadge />}
|
|
||||||
</ThemedView>
|
|
||||||
</ScrollView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
scrollView: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
contentContainer: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
container: {
|
|
||||||
maxWidth: MaxContentWidth,
|
|
||||||
flexGrow: 1,
|
|
||||||
},
|
|
||||||
titleContainer: {
|
|
||||||
gap: Spacing.three,
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingHorizontal: Spacing.four,
|
|
||||||
paddingVertical: Spacing.six,
|
|
||||||
},
|
|
||||||
centerText: {
|
|
||||||
textAlign: 'center',
|
|
||||||
},
|
|
||||||
pressed: {
|
|
||||||
opacity: 0.7,
|
|
||||||
},
|
|
||||||
linkButton: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
paddingHorizontal: Spacing.four,
|
|
||||||
paddingVertical: Spacing.two,
|
|
||||||
borderRadius: Spacing.five,
|
|
||||||
justifyContent: 'center',
|
|
||||||
gap: Spacing.one,
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
sectionsWrapper: {
|
|
||||||
gap: Spacing.five,
|
|
||||||
paddingHorizontal: Spacing.four,
|
|
||||||
paddingTop: Spacing.three,
|
|
||||||
},
|
|
||||||
collapsibleContent: {
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
imageTutorial: {
|
|
||||||
width: '100%',
|
|
||||||
aspectRatio: 296 / 171,
|
|
||||||
borderRadius: Spacing.three,
|
|
||||||
marginTop: Spacing.two,
|
|
||||||
},
|
|
||||||
imageReact: {
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
alignSelf: 'center',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,98 +1,252 @@
|
|||||||
import * as Device from 'expo-device';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { Platform, StyleSheet } from 'react-native';
|
import { StyleSheet, View, Text, FlatList, TouchableOpacity, PermissionsAndroid, Platform } from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import { Stack, router } from 'expo-router';
|
||||||
|
import { useKeepAwake } from 'expo-keep-awake';
|
||||||
|
import io from 'socket.io-client';
|
||||||
|
import SendDirectSms from 'react-native-send-direct-sms';
|
||||||
|
|
||||||
import { AnimatedIcon } from '@/components/animated-icon';
|
export default function DashboardScreen() {
|
||||||
import { HintRow } from '@/components/hint-row';
|
useKeepAwake(); // Keep the screen awake
|
||||||
import { ThemedText } from '@/components/themed-text';
|
|
||||||
import { ThemedView } from '@/components/themed-view';
|
|
||||||
import { WebBadge } from '@/components/web-badge';
|
|
||||||
import { BottomTabInset, MaxContentWidth, Spacing } from '@/constants/theme';
|
|
||||||
|
|
||||||
function getDevMenuHint() {
|
const [status, setStatus] = useState('Disconnected');
|
||||||
if (Platform.OS === 'web') {
|
const [hasPermission, setHasPermission] = useState(false);
|
||||||
return <ThemedText type="small">use browser devtools</ThemedText>;
|
const [logs, setLogs] = useState<{id: string, time: string, msg: string}[]>([]);
|
||||||
|
const socketRef = useRef<any>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkAndRequestPermission();
|
||||||
|
connectSocket();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (socketRef.current) {
|
||||||
|
socketRef.current.disconnect();
|
||||||
}
|
}
|
||||||
if (Device.isDevice) {
|
};
|
||||||
return (
|
}, []);
|
||||||
<ThemedText type="small">
|
|
||||||
shake device or press <ThemedText type="code">m</ThemedText> in terminal
|
const checkAndRequestPermission = async () => {
|
||||||
</ThemedText>
|
if (Platform.OS === 'android') {
|
||||||
|
try {
|
||||||
|
const granted = await PermissionsAndroid.request(
|
||||||
|
PermissionsAndroid.PERMISSIONS.SEND_SMS,
|
||||||
|
{
|
||||||
|
title: 'SMS Permission',
|
||||||
|
message: 'This app needs access to send SMS messages directly.',
|
||||||
|
buttonNeutral: 'Ask Me Later',
|
||||||
|
buttonNegative: 'Cancel',
|
||||||
|
buttonPositive: 'OK',
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
if (granted === PermissionsAndroid.RESULTS.GRANTED) {
|
||||||
|
setHasPermission(true);
|
||||||
|
addLog('SMS permission granted');
|
||||||
|
} else {
|
||||||
|
setHasPermission(false);
|
||||||
|
addLog('SMS permission denied');
|
||||||
}
|
}
|
||||||
const shortcut = Platform.OS === 'android' ? 'cmd+m (or ctrl+m)' : 'cmd+d';
|
} catch (err) {
|
||||||
return (
|
console.warn(err);
|
||||||
<ThemedText type="small">
|
addLog(`Permission error: ${err}`);
|
||||||
press <ThemedText type="code">{shortcut}</ThemedText>
|
}
|
||||||
</ThemedText>
|
}
|
||||||
);
|
};
|
||||||
|
|
||||||
|
const addLog = (msg: string) => {
|
||||||
|
setLogs(prev => [{
|
||||||
|
id: Math.random().toString(),
|
||||||
|
time: new Date().toLocaleTimeString(),
|
||||||
|
msg
|
||||||
|
}, ...prev].slice(0, 50)); // Keep last 50 logs
|
||||||
|
};
|
||||||
|
|
||||||
|
const connectSocket = async () => {
|
||||||
|
try {
|
||||||
|
const endpoint = await AsyncStorage.getItem('endpoint');
|
||||||
|
const login = await AsyncStorage.getItem('login');
|
||||||
|
const password = await AsyncStorage.getItem('password');
|
||||||
|
|
||||||
|
if (!endpoint || !login || !password) {
|
||||||
|
setStatus('Missing Settings');
|
||||||
|
addLog('Please configure settings first');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function HomeScreen() {
|
if (socketRef.current) {
|
||||||
|
socketRef.current.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus('Connecting...');
|
||||||
|
|
||||||
|
const socket = io(endpoint, {
|
||||||
|
auth: { login, password },
|
||||||
|
reconnectionAttempts: Infinity, // Keep trying forever
|
||||||
|
reconnectionDelay: 1000,
|
||||||
|
reconnectionDelayMax: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('connect', () => {
|
||||||
|
setStatus('Connected');
|
||||||
|
addLog('Connected to backend');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
setStatus('Disconnected');
|
||||||
|
addLog('Disconnected from backend');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('connect_error', (err) => {
|
||||||
|
setStatus('Error');
|
||||||
|
addLog(`Connection error: ${err.message}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('send_sms', async (data) => {
|
||||||
|
addLog(`Received SMS request ID: ${data.id} to ${data.phone}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Default to rule phone if empty, or use the one provided
|
||||||
|
const targetPhone = data.phone || '+993 61 92 92 48';
|
||||||
|
|
||||||
|
if (Platform.OS === 'android') {
|
||||||
|
SendDirectSms.sendDirectSms(targetPhone, data.message)
|
||||||
|
.then((res: any) => {
|
||||||
|
addLog(`SMS sent successfully to ${targetPhone}`);
|
||||||
|
socket.emit('sms_status', { id: data.id, status: 'sent' });
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
addLog(`SMS send failed: ${err}`);
|
||||||
|
socket.emit('sms_status', { id: data.id, status: 'failed' });
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
addLog('Direct SMS only supported on Android');
|
||||||
|
socket.emit('sms_status', { id: data.id, status: 'failed' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
addLog(`Error sending SMS: ${error}`);
|
||||||
|
socket.emit('sms_status', { id: data.id, status: 'failed' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socketRef.current = socket;
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
addLog(`Setup error: ${e}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemedView style={styles.container}>
|
<View style={styles.container}>
|
||||||
<SafeAreaView style={styles.safeArea}>
|
<View style={styles.statusCard}>
|
||||||
<ThemedView style={styles.heroSection}>
|
<Text style={styles.statusLabel}>Backend Status:</Text>
|
||||||
<AnimatedIcon />
|
<Text style={[
|
||||||
<ThemedText type="title" style={styles.title}>
|
styles.statusValue,
|
||||||
Welcome to Expo
|
{ color: status === 'Connected' ? 'green' : (status === 'Error' || status === 'Disconnected' ? 'red' : 'orange') }
|
||||||
</ThemedText>
|
]}>
|
||||||
</ThemedView>
|
{status}
|
||||||
|
</Text>
|
||||||
|
<View style={styles.buttonRow}>
|
||||||
|
<TouchableOpacity style={styles.reconnectButton} onPress={connectSocket}>
|
||||||
|
<Text style={styles.reconnectText}>Reconnect</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
<ThemedText type="code" style={styles.code}>
|
{!hasPermission && (
|
||||||
get started
|
<TouchableOpacity style={[styles.reconnectButton, { backgroundColor: '#ffebee', marginLeft: 10 }]} onPress={checkAndRequestPermission}>
|
||||||
</ThemedText>
|
<Text style={[styles.reconnectText, { color: '#c62828' }]}>Req Permission</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
<ThemedView type="backgroundElement" style={styles.stepContainer}>
|
<Text style={styles.logTitle}>Activity Log</Text>
|
||||||
<HintRow
|
<FlatList
|
||||||
title="Try editing"
|
data={logs}
|
||||||
hint={<ThemedText type="code">src/app/index.tsx</ThemedText>}
|
keyExtractor={item => item.id}
|
||||||
|
style={styles.logList}
|
||||||
|
renderItem={({item}) => (
|
||||||
|
<View style={styles.logItem}>
|
||||||
|
<Text style={styles.logTime}>{item.time}</Text>
|
||||||
|
<Text style={styles.logMsg}>{item.msg}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<HintRow title="Dev tools" hint={getDevMenuHint()} />
|
</View>
|
||||||
<HintRow
|
|
||||||
title="Fresh start"
|
|
||||||
hint={<ThemedText type="code">npm run reset-project</ThemedText>}
|
|
||||||
/>
|
|
||||||
</ThemedView>
|
|
||||||
|
|
||||||
{Platform.OS === 'web' && <WebBadge />}
|
|
||||||
</SafeAreaView>
|
|
||||||
</ThemedView>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
backgroundColor: '#f5f5f5',
|
||||||
|
},
|
||||||
|
headerButton: {
|
||||||
|
marginRight: 15,
|
||||||
|
},
|
||||||
|
headerButtonText: {
|
||||||
|
color: '#208AEF',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
statusCard: {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
margin: 15,
|
||||||
|
padding: 20,
|
||||||
|
borderRadius: 10,
|
||||||
|
alignItems: 'center',
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 3,
|
||||||
|
},
|
||||||
|
statusLabel: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: '#666',
|
||||||
|
},
|
||||||
|
statusValue: {
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginTop: 5,
|
||||||
|
},
|
||||||
|
reconnectButton: {
|
||||||
|
paddingVertical: 8,
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
backgroundColor: '#eee',
|
||||||
|
borderRadius: 20,
|
||||||
|
},
|
||||||
|
buttonRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
marginTop: 15,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
reconnectText: {
|
||||||
|
color: '#333',
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
logTitle: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginLeft: 15,
|
||||||
|
marginBottom: 10,
|
||||||
|
color: '#333',
|
||||||
|
},
|
||||||
|
logList: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
},
|
||||||
|
logItem: {
|
||||||
|
padding: 15,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: '#eee',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
},
|
},
|
||||||
safeArea: {
|
logTime: {
|
||||||
|
color: '#888',
|
||||||
|
fontSize: 12,
|
||||||
|
width: 80,
|
||||||
|
},
|
||||||
|
logMsg: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
paddingHorizontal: Spacing.four,
|
color: '#333',
|
||||||
alignItems: 'center',
|
fontSize: 14,
|
||||||
gap: Spacing.three,
|
}
|
||||||
paddingBottom: BottomTabInset + Spacing.three,
|
|
||||||
maxWidth: MaxContentWidth,
|
|
||||||
},
|
|
||||||
heroSection: {
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
flex: 1,
|
|
||||||
paddingHorizontal: Spacing.four,
|
|
||||||
gap: Spacing.four,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
textAlign: 'center',
|
|
||||||
},
|
|
||||||
code: {
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
},
|
|
||||||
stepContainer: {
|
|
||||||
gap: Spacing.three,
|
|
||||||
alignSelf: 'stretch',
|
|
||||||
paddingHorizontal: Spacing.three,
|
|
||||||
paddingVertical: Spacing.four,
|
|
||||||
borderRadius: Spacing.four,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
145
src/app/settings.tsx
Normal file
145
src/app/settings.tsx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { StyleSheet, View, Text, TextInput, TouchableOpacity, Switch, ScrollView } from 'react-native';
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import { Stack } from 'expo-router';
|
||||||
|
import { useKeepAwake } from 'expo-keep-awake';
|
||||||
|
// We will import socket logic later
|
||||||
|
|
||||||
|
export default function SettingsScreen() {
|
||||||
|
useKeepAwake(); // Keep the screen awake
|
||||||
|
|
||||||
|
const [endpoint, setEndpoint] = useState('http://192.168.1.x:3001');
|
||||||
|
const [login, setLogin] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [isSaved, setIsSaved] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadSettings();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadSettings = async () => {
|
||||||
|
try {
|
||||||
|
const savedEndpoint = await AsyncStorage.getItem('endpoint');
|
||||||
|
const savedLogin = await AsyncStorage.getItem('login');
|
||||||
|
const savedPassword = await AsyncStorage.getItem('password');
|
||||||
|
|
||||||
|
if (savedEndpoint) setEndpoint(savedEndpoint);
|
||||||
|
if (savedLogin) setLogin(savedLogin);
|
||||||
|
if (savedPassword) setPassword(savedPassword);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load settings', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveSettings = async () => {
|
||||||
|
try {
|
||||||
|
await AsyncStorage.setItem('endpoint', endpoint);
|
||||||
|
await AsyncStorage.setItem('login', login);
|
||||||
|
await AsyncStorage.setItem('password', password);
|
||||||
|
setIsSaved(true);
|
||||||
|
setTimeout(() => setIsSaved(false), 2000);
|
||||||
|
|
||||||
|
// In a real app, we would trigger a reconnect here
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to save settings', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView style={styles.container}>
|
||||||
|
<View style={styles.section}>
|
||||||
|
<Text style={styles.label}>Backend Endpoint</Text>
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
value={endpoint}
|
||||||
|
onChangeText={setEndpoint}
|
||||||
|
placeholder="http://192.168.1.x:3001"
|
||||||
|
autoCapitalize="none"
|
||||||
|
keyboardType="url"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.section}>
|
||||||
|
<Text style={styles.label}>Login</Text>
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
value={login}
|
||||||
|
onChangeText={setLogin}
|
||||||
|
placeholder="admin"
|
||||||
|
autoCapitalize="none"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.section}>
|
||||||
|
<Text style={styles.label}>Password</Text>
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
value={password}
|
||||||
|
onChangeText={setPassword}
|
||||||
|
placeholder="admin123"
|
||||||
|
secureTextEntry
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity style={styles.button} onPress={saveSettings}>
|
||||||
|
<Text style={styles.buttonText}>{isSaved ? 'Saved!' : 'Save Settings'}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<View style={styles.infoBox}>
|
||||||
|
<Text style={styles.infoText}>
|
||||||
|
Note: This app will keep the screen awake while running to ensure background SMS sending works reliably.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
padding: 20,
|
||||||
|
backgroundColor: '#f5f5f5',
|
||||||
|
},
|
||||||
|
section: {
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginBottom: 8,
|
||||||
|
color: '#333',
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#ddd',
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 12,
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
backgroundColor: '#208AEF',
|
||||||
|
padding: 15,
|
||||||
|
borderRadius: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: 10,
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
infoBox: {
|
||||||
|
marginTop: 30,
|
||||||
|
padding: 15,
|
||||||
|
backgroundColor: '#e8f4fd',
|
||||||
|
borderRadius: 8,
|
||||||
|
borderLeftWidth: 4,
|
||||||
|
borderLeftColor: '#208AEF',
|
||||||
|
},
|
||||||
|
infoText: {
|
||||||
|
color: '#555',
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: 20,
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -20,8 +20,8 @@ export default function AppTabs() {
|
|||||||
/>
|
/>
|
||||||
</NativeTabs.Trigger>
|
</NativeTabs.Trigger>
|
||||||
|
|
||||||
<NativeTabs.Trigger name="explore">
|
<NativeTabs.Trigger name="settings">
|
||||||
<NativeTabs.Trigger.Label>Explore</NativeTabs.Trigger.Label>
|
<NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label>
|
||||||
<NativeTabs.Trigger.Icon
|
<NativeTabs.Trigger.Icon
|
||||||
src={require('@/assets/images/tabIcons/explore.png')}
|
src={require('@/assets/images/tabIcons/explore.png')}
|
||||||
renderingMode="template"
|
renderingMode="template"
|
||||||
|
|||||||
Reference in New Issue
Block a user