TriloopTem_App/app/connect.tsx
2026-06-20 21:43:09 +08:00

232 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect, useRef } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
ScrollView,
Linking,
ActivityIndicator,
KeyboardAvoidingView,
Platform,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { router } from 'expo-router';
import { useConnectionStore } from '../src/stores/connectionStore';
import { useDevice } from '../src/hooks/useDevice';
import { useTheme } from '../src/design/tokens';
const LOG_MAX = 60;
export default function ConnectScreen() {
const theme = useTheme();
const { host, port, status, lastError, setHost, setPort } = useConnectionStore();
const { connect, disconnect } = useDevice();
const [logs, setLogs] = useState<string[]>(['Ready.']);
const [portStr, setPortStr] = useState(String(port));
const prevStatusRef = useRef(status);
const addLog = (msg: string) => {
setLogs((prev) => [`[${new Date().toLocaleTimeString()}] ${msg}`, ...prev].slice(0, LOG_MAX));
};
useEffect(() => {
const prev = prevStatusRef.current;
prevStatusRef.current = status;
if (status === prev) return;
if (status === 'connected') {
addLog('连接成功 ✓');
router.push('/(tabs)/control');
} else if (status === 'error') {
addLog(`错误: ${lastError}`);
} else if (status === 'reconnecting') {
addLog('断线,正在重连...');
}
}, [status, lastError]);
const handleConnect = () => {
const p = parseInt(portStr, 10);
if (!host || isNaN(p)) { addLog('请检查 IP 和端口'); return; }
setPort(p);
addLog(`正在连接 ${host}:${p} ...`);
connect();
};
const handleDisconnect = () => {
disconnect();
addLog('已断开连接');
};
const isConnecting = status === 'connecting' || status === 'reconnecting';
const isConnected = status === 'connected';
return (
<SafeAreaView style={[S.safe, { backgroundColor: theme.bg.base }]}>
<KeyboardAvoidingView style={S.flex} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}>
<ScrollView style={S.scroll} contentContainerStyle={S.scrollContent} keyboardShouldPersistTaps="handled">
{/* ── Brand ── */}
<View style={S.brand}>
<Text style={[S.brandIcon, { color: theme.blue.fg }]}></Text>
<Text style={[S.title, { color: theme.text.primary }]}>TEM Receiver</Text>
<Text style={[S.subtitle, { color: theme.text.muted }]}></Text>
</View>
{/* ── Setup guide ── */}
<View style={[S.card, { backgroundColor: theme.bg.raised, borderColor: theme.bg.border }]}>
<Text style={[S.cardLabel, { color: theme.text.muted }]}></Text>
<View style={S.stepRow}>
<View style={[S.stepBadge, { backgroundColor: theme.blue.bg }]}><Text style={[S.stepNum, { color: theme.blue.fg }]}>1</Text></View>
<Text style={[S.stepText, { color: theme.text.secondary }]}> WiFi </Text>
<TouchableOpacity style={[S.linkBtn, { backgroundColor: theme.blue.bg, borderColor: theme.blue.border }]} onPress={() => Linking.openSettings()}>
<Text style={[S.linkBtnText, { color: theme.blue.fg }]}> </Text>
</TouchableOpacity>
</View>
<View style={S.stepRow}>
<View style={[S.stepBadge, { backgroundColor: theme.blue.bg }]}><Text style={[S.stepNum, { color: theme.blue.fg }]}>2</Text></View>
<Text style={[S.stepText, { color: theme.text.secondary }]}></Text>
</View>
</View>
{/* ── Inputs ── */}
<View style={[S.card, { backgroundColor: theme.bg.raised, borderColor: theme.bg.border }]}>
<Text style={[S.cardLabel, { color: theme.text.muted }]}></Text>
<View style={S.inputRow}>
<Text style={[S.inputLabel, { color: theme.text.secondary }]}>IP </Text>
<TextInput
style={[S.input, { color: theme.text.primary, borderBottomColor: theme.bg.border }]}
value={host}
onChangeText={setHost}
keyboardType="numeric"
placeholder="192.168.4.1"
placeholderTextColor={theme.text.muted}
autoCapitalize="none"
/>
</View>
<View style={[S.inputDivider, { backgroundColor: theme.bg.border }]} />
<View style={S.inputRow}>
<Text style={[S.inputLabel, { color: theme.text.secondary }]}> </Text>
<TextInput
style={[S.input, { color: theme.text.primary, borderBottomColor: theme.bg.border }]}
value={portStr}
onChangeText={setPortStr}
keyboardType="numeric"
placeholder="4321"
placeholderTextColor={theme.text.muted}
/>
</View>
</View>
{/* ── Action ── */}
{!isConnected ? (
<TouchableOpacity
style={[S.connectBtn, { backgroundColor: theme.blue.border, borderColor: theme.blue.fg }, isConnecting && { borderColor: theme.text.ghost, backgroundColor: theme.blue.bg }]}
onPress={handleConnect}
disabled={isConnecting}
activeOpacity={0.8}
>
{isConnecting
? <ActivityIndicator color={theme.text.primary} />
: <Text style={[S.connectBtnText, { color: theme.blue.fg }]}> </Text>}
</TouchableOpacity>
) : (
<View style={S.connectedBox}>
<View style={S.connectedLeft}>
<View style={[S.greenDot, { backgroundColor: theme.green.fg }]} />
<Text style={[S.connectedText, { color: theme.green.fg }]}></Text>
</View>
<TouchableOpacity style={[S.goBtn, { backgroundColor: theme.green.bg, borderColor: theme.green.border }]} onPress={() => router.push('/(tabs)/control')} activeOpacity={0.8}>
<Text style={[S.goBtnText, { color: theme.green.fg }]}> </Text>
</TouchableOpacity>
<TouchableOpacity style={[S.disconnectBtn, { backgroundColor: theme.red.bg, borderColor: theme.red.border }]} onPress={handleDisconnect} activeOpacity={0.8}>
<Text style={[S.disconnectBtnText, { color: theme.red.fg }]}></Text>
</TouchableOpacity>
</View>
)}
{/* ── Log terminal ── */}
<View style={[S.terminal, { backgroundColor: theme.bg.void, borderColor: theme.bg.border }]}>
<Text style={[S.terminalHeader, { color: theme.green.fg, borderBottomColor: theme.bg.divider }]}> </Text>
<View style={S.terminalBody}>
{logs.map((l, i) => (
<Text key={i} style={[S.terminalLine, { color: theme.text.muted }, i === 0 && { color: theme.text.secondary }]}>{l}</Text>
))}
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
const S = StyleSheet.create({
safe: { flex: 1 },
flex: { flex: 1 },
scroll: { flex: 1 },
scrollContent: { padding: 20, paddingBottom: 40 },
// Brand
brand: { alignItems: 'center', marginBottom: 28, marginTop: 8 },
brandIcon: { fontSize: 36, marginBottom: 8 },
title: { fontSize: 26, fontWeight: '700', letterSpacing: 1 },
subtitle: { fontSize: 12, marginTop: 4, letterSpacing: 0.5 },
// Card
card: {
borderRadius: 14,
borderWidth: 1,
padding: 16,
marginBottom: 12,
},
cardLabel: { fontSize: 10, fontWeight: '600', letterSpacing: 1.5, marginBottom: 12, textTransform: 'uppercase' },
// Steps
stepRow: { flexDirection: 'row', alignItems: 'center', gap: 10, marginBottom: 8 },
stepBadge: { width: 20, height: 20, borderRadius: 10, justifyContent: 'center', alignItems: 'center' },
stepNum: { fontSize: 11, fontWeight: '700' },
stepText: { fontSize: 13, flex: 1 },
linkBtn: { borderRadius: 6, paddingHorizontal: 10, paddingVertical: 4, borderWidth: 1 },
linkBtnText: { fontSize: 11, fontWeight: '600' },
// Inputs
inputRow: { flexDirection: 'row', alignItems: 'center', gap: 12 },
inputDivider: { height: StyleSheet.hairlineWidth, marginVertical: 10 },
inputLabel: { fontSize: 12, width: 54, letterSpacing: 0.3 },
input: {
flex: 1,
fontSize: 15,
fontWeight: '500',
paddingVertical: 6,
borderBottomWidth: 1,
},
// Connect button
connectBtn: {
borderRadius: 14,
paddingVertical: 16,
alignItems: 'center',
marginBottom: 12,
borderWidth: 1,
marginTop: 4,
},
connectBtnText: { fontSize: 16, fontWeight: '700', letterSpacing: 3 },
// Connected state
connectedBox: { flexDirection: 'row', alignItems: 'center', gap: 10, marginBottom: 12, marginTop: 4 },
connectedLeft: { flexDirection: 'row', alignItems: 'center', gap: 6 },
greenDot: { width: 8, height: 8, borderRadius: 4 },
connectedText: { fontWeight: '700', fontSize: 13 },
goBtn: { flex: 1, borderRadius: 10, paddingVertical: 11, alignItems: 'center', borderWidth: 1 },
goBtnText: { fontWeight: '600', fontSize: 13 },
disconnectBtn: { borderRadius: 10, paddingVertical: 11, paddingHorizontal: 14, borderWidth: 1 },
disconnectBtnText: { fontSize: 13, fontWeight: '600' },
// Terminal log
terminal: { borderRadius: 12, borderWidth: 1, overflow: 'hidden', minHeight: 120 },
terminalHeader: { fontSize: 10, fontWeight: '700', letterSpacing: 1.5, paddingHorizontal: 14, paddingTop: 10, paddingBottom: 6, borderBottomWidth: 1 },
terminalBody: { padding: 12 },
terminalLine: { fontSize: 10, fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace', marginBottom: 3, lineHeight: 16 },
});