TriloopTem_App/app/(tabs)/control.tsx
2026-06-19 22:08:10 +08:00

220 lines
8.3 KiB
TypeScript

import React, { useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, ScrollView, ActivityIndicator, Platform } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { router } from 'expo-router';
import { DeviceStatusBar } from '../../src/components/DeviceStatusBar';
import { ParamForm } from '../../src/components/ParamForm';
import { useDevice } from '../../src/hooks/useDevice';
import { useDataStore } from '../../src/stores/dataStore';
export default function ControlScreen() {
const insets = useSafeAreaInsets();
const { busy, connected, deviceStatus, setup, startContinuous, startSingle, stop } = useDevice();
const frame = useDataStore((s) => s.currentFrame);
const sessionId = useDataStore((s) => s.sessionId);
const projectId = useDataStore((s) => s.projectId);
const [configDirty, setConfigDirty] = useState(false);
const [projectName, setProjectName] = React.useState<string | null>(null);
React.useEffect(() => {
if (!projectId) { setProjectName(null); return; }
import('../../src/services/StorageService').then(({ listProjects }) =>
listProjects().then((ps) => setProjectName(ps.find(p => p.projectId === projectId)?.name ?? null)),
);
}, [projectId]);
const handleSetup = async () => {
await setup();
setConfigDirty(false);
};
if (!connected) {
return (
<View style={[S.notConnected, { paddingTop: insets.top }]}>
<Text style={S.notConnectedIcon}></Text>
<Text style={S.notConnectedText}></Text>
<TouchableOpacity style={S.goConnectBtn} onPress={() => router.push('/connect')} activeOpacity={0.8}>
<Text style={S.goConnectBtnText}></Text>
</TouchableOpacity>
</View>
);
}
const running = deviceStatus === 'running';
const single = deviceStatus === 'single';
const canStart = !running && !single && !busy;
const canStop = (running || single) && !busy;
return (
<View style={[S.container, { paddingTop: insets.top }]}>
<DeviceStatusBar />
{/* Project / line context */}
<View style={S.contextBar}>
{projectName
? <Text style={S.contextText} numberOfLines={1}>{projectName} · {sessionId}</Text>
: <Text style={S.contextText} numberOfLines={1}>{sessionId}</Text>
}
</View>
{/* Control buttons */}
<View style={S.ctrlRow}>
<TouchableOpacity
style={[S.ctrlBtn, S.btnStart, !canStart && S.btnDisabled]}
onPress={startContinuous}
disabled={!canStart}
activeOpacity={0.8}
>
{busy && !canStop
? <ActivityIndicator color="#3ddc84" size="small" />
: <>
<Text style={S.ctrlIcon}></Text>
<Text style={[S.ctrlText, S.ctrlTextGreen]}></Text>
</>
}
</TouchableOpacity>
<TouchableOpacity
style={[S.ctrlBtn, S.btnSingle, !canStart && S.btnDisabled]}
onPress={startSingle}
disabled={!canStart}
activeOpacity={0.8}
>
<Text style={S.ctrlIcon}></Text>
<Text style={[S.ctrlText, S.ctrlTextBlue]}> </Text>
</TouchableOpacity>
<TouchableOpacity
style={[S.ctrlBtn, S.btnStop, !canStop && S.btnDisabled]}
onPress={stop}
disabled={!canStop}
activeOpacity={0.8}
>
{busy && canStop
? <ActivityIndicator color="#ff5c6e" size="small" />
: <>
<Text style={S.ctrlIcon}></Text>
<Text style={[S.ctrlText, S.ctrlTextRed]}> </Text>
</>
}
</TouchableOpacity>
</View>
{/* Frame info */}
{frame && (
<View style={S.frameBar}>
<View style={S.frameItem}>
<Text style={S.frameItemLabel}></Text>
<Text style={S.frameItemValue}>#{frame.frameId}</Text>
</View>
<View style={S.frameSep} />
<View style={S.frameItem}>
<Text style={S.frameItemLabel}></Text>
<Text style={S.frameItemValue}>{frame.accNum}</Text>
</View>
<View style={S.frameSep} />
<View style={S.frameItem}>
<Text style={S.frameItemLabel}></Text>
<Text style={S.frameItemValue}>{frame.meta?.channelNum ?? '-'}ch</Text>
</View>
<View style={S.frameSep} />
<View style={S.frameItem}>
<Text style={S.frameItemLabel}></Text>
<Text style={S.frameItemValue}>{frame.meta?.current ?? '--'}</Text>
</View>
</View>
)}
{/* Param form */}
<ScrollView style={S.formScroll} keyboardShouldPersistTaps="handled">
<View style={S.formHeader}>
<Text style={S.formTitle}></Text>
{configDirty && (
<View style={S.dirtyBadge}>
<Text style={S.dirtyBadgeText}></Text>
</View>
)}
</View>
<ParamForm onAnyChange={() => setConfigDirty(true)} />
<TouchableOpacity
style={[S.setupBtn, busy && S.btnDisabled]}
onPress={handleSetup}
disabled={busy}
activeOpacity={0.8}
>
{busy
? <ActivityIndicator color="#4a9eff" />
: <Text style={S.setupBtnText}> </Text>}
</TouchableOpacity>
<View style={{ height: 32 }} />
</ScrollView>
</View>
);
}
const S = StyleSheet.create({
container: { flex: 1, backgroundColor: '#090912' },
notConnected: { flex: 1, justifyContent: 'center', alignItems: 'center', gap: 12, backgroundColor: '#090912' },
notConnectedIcon: { fontSize: 48, color: '#2a2a4a' },
notConnectedText: { color: '#4a4a6a', fontSize: 15 },
goConnectBtn: { backgroundColor: '#111120', borderRadius: 10, paddingVertical: 10, paddingHorizontal: 28, borderWidth: 1, borderColor: '#4a9eff' },
goConnectBtnText: { color: '#4a9eff', fontWeight: '600' },
// Control buttons
ctrlRow: { flexDirection: 'row', gap: 8, padding: 12 },
ctrlBtn: {
flex: 1,
paddingVertical: 14,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
gap: 4,
borderWidth: 1,
},
btnStart: { backgroundColor: '#0d2018', borderColor: '#1a4a28' },
btnSingle: { backgroundColor: '#0d1a30', borderColor: '#1a2e54' },
btnStop: { backgroundColor: '#1e0d12', borderColor: '#3a1520' },
btnDisabled: { opacity: 0.3 },
ctrlIcon: { fontSize: 12, color: '#ffffff88' },
ctrlText: { fontSize: 12, fontWeight: '700', letterSpacing: 1 },
ctrlTextGreen: { color: '#3ddc84' },
ctrlTextBlue: { color: '#4a9eff' },
ctrlTextRed: { color: '#ff5c6e' },
// Frame info bar
frameBar: {
flexDirection: 'row',
marginHorizontal: 12,
marginBottom: 8,
backgroundColor: '#0c0c1a',
borderRadius: 10,
borderWidth: 1,
borderColor: '#1a1a2a',
overflow: 'hidden',
},
frameItem: { flex: 1, alignItems: 'center', paddingVertical: 8 },
frameItemLabel: { color: '#3a3a5a', fontSize: 9, fontWeight: '600', letterSpacing: 0.5, marginBottom: 2 },
frameItemValue: { color: '#8888aa', fontSize: 12, fontWeight: '600', fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace' },
frameSep: { width: StyleSheet.hairlineWidth, backgroundColor: '#1e1e30', marginVertical: 6 },
// Form
formScroll: { flex: 1 },
formHeader: { flexDirection: 'row', alignItems: 'center', gap: 10, paddingHorizontal: 16, paddingVertical: 10 },
formTitle: { color: '#4a4a6a', fontSize: 10, fontWeight: '700', letterSpacing: 1.5, textTransform: 'uppercase' },
dirtyBadge: { backgroundColor: '#2a1a05', borderRadius: 4, paddingHorizontal: 6, paddingVertical: 2, borderWidth: 1, borderColor: '#4a2a05' },
dirtyBadgeText: { color: '#ffb84d', fontSize: 9, fontWeight: '700' },
setupBtn: {
margin: 16,
backgroundColor: '#111120',
borderRadius: 12,
paddingVertical: 14,
alignItems: 'center',
borderWidth: 1,
borderColor: '#4a9eff',
},
setupBtnText: { color: '#4a9eff', fontWeight: '700', fontSize: 14, letterSpacing: 3 },
contextBar: { paddingHorizontal: 16, paddingVertical: 5, backgroundColor: '#08080f', borderBottomWidth: 1, borderBottomColor: '#141422' },
contextText: { color: '#2a2a4a', fontSize: 9, fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace' },
});