import { useFocusEffect } from '@react-navigation/native'; import { CameraView, useCameraPermissions } from 'expo-camera'; import { useKeepAwake } from 'expo-keep-awake'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Alert, Image, Modal, Pressable, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, View, } from 'react-native'; import { useApp } from '@/src/app-context'; function PromptModal({ visible, title, placeholder, value, onChange, onCancel, onConfirm, }: { visible: boolean; title: string; placeholder: string; value: string; onChange: (value: string) => void; onCancel: () => void; onConfirm: () => void; }) { return ( {title} Cancel Save ); } function CameraDashboard() { const { state, actions } = useApp(); const cameraViewRef = useRef(null); const [permission, requestPermission] = useCameraPermissions(); // Prevent auto-lock while the camera dashboard is open in the foreground. useKeepAwake('securecam-camera-dashboard'); useEffect(() => { actions.setCameraPermissionGranted(Boolean(permission?.granted)); }, [actions, permission?.granted]); useEffect(() => { return () => { actions.setCameraRef(null); actions.setCameraPreviewReady(false); }; }, [actions]); return ( Camera Preview {!permission?.granted ? ( Camera permission is required to publish live frames. void requestPermission()}> Grant Camera Permission ) : ( { cameraViewRef.current = ref; actions.setCameraRef(ref); }} style={styles.previewCamera} facing="back" onCameraReady={() => actions.setCameraPreviewReady(true)} /> )} Camera Status Realtime {state.socketConnected ? 'ONLINE' : 'OFFLINE'} Motion {state.isMotionActive ? 'ACTIVE' : 'IDLE'} Preview {state.cameraPreviewReady ? 'READY' : 'NOT READY'} Publishing {state.cameraStatus === 'recording' ? 'ACTIVE' : 'IDLE'} Actions {!state.isMotionActive ? ( void actions.startMotion()}> Simulate Motion Event ) : ( void actions.endMotion()}> Stop Motion Event )} void actions.goOnline()}> Reconnect Realtime This mobile build remains on the legacy frame-relay path. Add a native WebRTC stack before enabling SIMPLE_STREAMING by default. Logs {state.activityLog.length === 0 ? ( Awaiting events... ) : ( state.activityLog.map((item) => ( [{new Date(item.createdAt).toLocaleTimeString()}] {item.type}: {item.message} )) )} ); } function ClientDashboard() { const { state, actions } = useApp(); const [linkModalOpen, setLinkModalOpen] = useState(false); const [cameraIdInput, setCameraIdInput] = useState(''); const [renameTargetId, setRenameTargetId] = useState(null); const [renameInput, setRenameInput] = useState(''); const activeCameraLabel = useMemo(() => { const linked = state.linkedCameras.find((camera) => camera.cameraDeviceId === state.activeCameraDeviceId); return linked?.cameraName || linked?.cameraDeviceId || 'Live Feed Viewer'; }, [state.activeCameraDeviceId, state.linkedCameras]); const isCameraLive = (cameraDeviceId: string) => { const sessionId = state.cameraSessions?.[cameraDeviceId]; if (!sessionId) return false; return state.connectedStreamSessionIds.includes(sessionId); }; const openRenameModal = (cameraDeviceId: string, currentName?: string | null) => { setRenameTargetId(cameraDeviceId); setRenameInput(currentName || ''); }; const submitRename = async () => { if (!renameTargetId) return; await actions.renameLinkedCamera(renameTargetId, renameInput); setRenameTargetId(null); setRenameInput(''); }; return ( <> Client Dashboard setLinkModalOpen(true)}> Link Camera void actions.refreshClientData()}> R Your Cameras {state.linkedCameras.length === 0 ? ( No cameras linked yet ) : ( state.linkedCameras.map((link) => { const selected = state.activeCameraDeviceId === link.cameraDeviceId; const live = isCameraLive(link.cameraDeviceId); return ( {link.cameraName || link.cameraDeviceId} {(link.cameraStatus || 'offline').toUpperCase()} {live ? 'Live stream active' : 'Tap view to request stream'} void actions.selectCamera(link.cameraDeviceId)}> View openRenameModal(link.cameraDeviceId, link.cameraName)}> Rename { Alert.alert( 'Remove Camera', `Remove \"${link.cameraName || link.cameraDeviceId}\" from linked cameras?`, [ { text: 'Cancel', style: 'cancel' }, { text: 'Delete', style: 'destructive', onPress: () => { void actions.deleteLinkedCamera(link.id); }, }, ], ); }}> Delete ); }) )} {state.activeCameraDeviceId ? ( Live Feed: {activeCameraLabel} X {state.clientStreamMode === 'image' && state.clientFallbackFrame ? ( ) : ( {state.clientPlaceholderText} )} ) : null} Recent Recordings {state.recordings.length === 0 ? ( No recordings found ) : ( state.recordings.slice(0, 5).map((recording) => ( {new Date(recording.createdAt).toLocaleString()} {recording.durationSeconds != null ? `${recording.durationSeconds}s` : 'Duration pending'} -{' '} {recording.status ?? 'unknown'} void actions.openRecording(recording.id)}> Open )) )} { setLinkModalOpen(false); setCameraIdInput(''); }} onConfirm={() => { void actions.linkCamera(cameraIdInput); setLinkModalOpen(false); setCameraIdInput(''); }} /> { setRenameTargetId(null); setRenameInput(''); }} onConfirm={() => { void submitRename(); }} /> ); } export default function DashboardScreen() { const { state, actions } = useApp(); useFocusEffect( React.useCallback(() => { actions.setPage(state.device?.role === 'camera' ? 'camera' : 'client'); return undefined; }, [actions, state.device?.role]), ); return {state.device?.role === 'camera' ? : }; } const styles = StyleSheet.create({ safe: { flex: 1, backgroundColor: '#0a0a0c', }, scrollContent: { padding: 14, gap: 12, }, screenTitle: { color: '#f9fafb', fontSize: 22, fontWeight: '700', }, rowBetween: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', gap: 10, }, rowGap: { flexDirection: 'row', gap: 8, alignItems: 'center', }, card: { borderRadius: 16, padding: 14, borderWidth: 1, borderColor: 'rgba(255,255,255,0.08)', backgroundColor: '#111218', gap: 10, }, cardTitle: { color: '#f9fafb', fontSize: 15, fontWeight: '600', }, previewBox: { height: 220, borderRadius: 12, overflow: 'hidden', borderWidth: 1, borderColor: 'rgba(255,255,255,0.1)', backgroundColor: '#08090d', }, previewCamera: { flex: 1, }, permissionBox: { borderRadius: 12, borderWidth: 1, borderColor: 'rgba(255,255,255,0.1)', backgroundColor: '#08090d', padding: 12, gap: 10, }, permissionText: { color: '#9ca3af', fontSize: 12, lineHeight: 18, }, label: { color: '#9ca3af', fontSize: 12, }, status: { fontSize: 12, fontWeight: '700', }, online: { color: '#34d399', }, offline: { color: '#f87171', }, primaryButton: { height: 44, borderRadius: 10, backgroundColor: '#2563eb', alignItems: 'center', justifyContent: 'center', }, primaryButtonText: { color: '#f9fafb', fontWeight: '700', fontSize: 14, }, secondaryButton: { height: 44, borderRadius: 10, borderWidth: 1, borderColor: 'rgba(255,255,255,0.12)', alignItems: 'center', justifyContent: 'center', }, secondaryButtonText: { color: '#d1d5db', fontWeight: '600', fontSize: 13, }, outlineButton: { height: 36, borderRadius: 9, paddingHorizontal: 12, borderWidth: 1, borderColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center', }, outlineButtonText: { color: '#d1d5db', fontSize: 12, fontWeight: '600', }, iconButton: { height: 36, width: 36, borderRadius: 9, borderWidth: 1, borderColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center', }, iconButtonText: { color: '#d1d5db', fontSize: 12, fontWeight: '700', }, infoText: { color: '#9ca3af', fontSize: 12, lineHeight: 18, }, emptyText: { color: '#6b7280', fontSize: 12, }, logLine: { color: '#9ca3af', fontSize: 12, lineHeight: 18, }, cameraRow: { gap: 10, }, emptyCameraCard: { borderRadius: 12, borderWidth: 1, borderStyle: 'dashed', borderColor: 'rgba(255,255,255,0.2)', padding: 16, minWidth: 240, }, cameraCard: { minWidth: 260, borderRadius: 12, borderWidth: 1, borderColor: 'rgba(255,255,255,0.08)', padding: 12, backgroundColor: '#0d0f15', gap: 6, }, cameraCardSelected: { borderColor: '#3b82f6', backgroundColor: '#111b2f', }, cameraName: { color: '#f3f4f6', fontWeight: '600', fontSize: 13, }, cameraStatus: { fontSize: 11, fontWeight: '700', }, cameraSubtext: { color: '#9ca3af', fontSize: 11, marginBottom: 4, }, cameraActionsRow: { flexDirection: 'row', gap: 6, }, smallButton: { borderRadius: 8, borderWidth: 1, borderColor: 'rgba(255,255,255,0.16)', paddingHorizontal: 10, height: 30, alignItems: 'center', justifyContent: 'center', }, smallButtonText: { color: '#d1d5db', fontSize: 11, fontWeight: '600', }, smallDangerButton: { borderColor: 'rgba(248,113,113,0.4)', }, smallDangerText: { color: '#fca5a5', }, streamBox: { height: 220, borderRadius: 12, borderWidth: 1, borderColor: 'rgba(255,255,255,0.1)', backgroundColor: '#08090d', alignItems: 'center', justifyContent: 'center', overflow: 'hidden', }, streamImage: { width: '100%', height: '100%', }, streamPlaceholder: { color: '#6b7280', fontWeight: '600', }, recordingRow: { borderRadius: 10, borderWidth: 1, borderColor: 'rgba(255,255,255,0.08)', backgroundColor: '#0d0f15', padding: 10, flexDirection: 'row', alignItems: 'center', gap: 8, }, recordingTitle: { color: '#e5e7eb', fontSize: 12, fontWeight: '500', }, recordingMeta: { color: '#6b7280', fontSize: 11, marginTop: 2, }, modalBackdrop: { flex: 1, backgroundColor: 'rgba(0,0,0,0.7)', alignItems: 'center', justifyContent: 'center', padding: 16, }, modalCard: { width: '100%', maxWidth: 360, borderRadius: 14, padding: 14, backgroundColor: '#111218', borderWidth: 1, borderColor: 'rgba(255,255,255,0.1)', gap: 10, }, modalTitle: { color: '#f3f4f6', fontSize: 16, fontWeight: '700', }, modalInput: { height: 46, borderRadius: 10, borderWidth: 1, borderColor: 'rgba(255,255,255,0.14)', backgroundColor: '#0a0a0e', color: '#f3f4f6', paddingHorizontal: 10, }, modalActions: { flexDirection: 'row', justifyContent: 'flex-end', gap: 8, }, modalButton: { height: 36, borderRadius: 8, minWidth: 76, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 10, }, modalCancel: { borderWidth: 1, borderColor: 'rgba(255,255,255,0.16)', }, modalConfirm: { backgroundColor: '#2563eb', }, modalCancelText: { color: '#d1d5db', fontWeight: '600', }, modalConfirmText: { color: '#f9fafb', fontWeight: '700', }, });