import { ContextMenu, Host, Button as SwiftUIButton } from '@expo/ui/swift-ui'; import { Ionicons } from '@expo/vector-icons'; import { SymbolView } from 'expo-symbols'; import React, { useCallback, useEffect, useState } from 'react'; import { ActivityIndicator, Alert, AppState, FlatList, RefreshControl, StyleSheet, Text, TouchableOpacity, View, } from 'react-native'; import { useColorScheme } from '../../hooks/use-color-scheme'; import api from '../../src/services/api'; import { Device } from '../../src/types'; import * as Burnt from 'burnt'; export default function DeviceListScreen() { const colorScheme = useColorScheme() ?? 'light'; const isDark = colorScheme === 'dark'; const bgColor = isDark ? '#0b0b0d' : '#f5f5f5'; const cardBg = isDark ? '#1c1c1e' : '#fff'; const textColor = isDark ? '#ffffff' : '#333333'; const subTextColor = isDark ? '#c6c6c8' : '#666666'; const activityColor = isDark ? '#0A84FF' : '#007AFF'; const [devices, setDevices] = useState([]); const [isLoading, setIsLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const fetchDevices = useCallback(async (showLoading = false) => { try { if (showLoading) setIsLoading(true); const data = await api.getDevices(); setDevices(data); } catch (error: any) { // For background/periodic refreshes, avoid interruptive alerts if (showLoading) { Alert.alert('Error', error.message || 'Failed to load devices'); } } finally { if (showLoading) setIsLoading(false); } }, []); useEffect(() => { fetchDevices(true); }, [fetchDevices]); useEffect(() => { let intervalId: number | null = null; const startPolling = () => { if (intervalId !== null) return; intervalId = setInterval(() => { fetchDevices(false); }, 10000) as unknown as number; }; const stopPolling = () => { if (intervalId !== null) { clearInterval(intervalId); intervalId = null; } }; // Start polling while app is active; pause when backgrounded startPolling(); const onAppStateChange = (nextAppState: string) => { if (nextAppState === 'active') { startPolling(); } else { stopPolling(); } }; const subscription = AppState.addEventListener('change', onAppStateChange); return () => { stopPolling(); subscription.remove(); }; }, [fetchDevices]); const onRefresh = async () => { setRefreshing(true); await fetchDevices(false); setRefreshing(false); }; const handleWake = async (device: Device) => { try { await api.wakeDevice(device.id); Burnt.toast({ title: 'Success', preset: 'done', message: `Waking ${device.name} up.`, }); } catch (error: any) { Burnt.toast({ title: 'Error', preset: 'error', message: error.message || `Failed to wake up ${device.name}.`, }); } }; const handleSleep = async (device: Device) => { Alert.alert('Confirm', `Send ${device.name} to sleep?`, [ { text: 'Cancel', style: 'cancel' }, { text: 'Sleep', style: 'destructive', onPress: async () => { try { await api.sleepDevice(device.id); Burnt.toast({ title: 'Success', preset: 'done', message: `Sending ${device.name} to sleep.`, }); } catch (error: any) { Burnt.toast({ title: 'Error', preset: 'error', message: error.message || `Failed to send ${device.name} to sleep.`, }); } }, }, ]); }; const handleReboot = async (device: Device) => { Alert.alert('Confirm', `Reboot ${device.name}?`, [ { text: 'Cancel', style: 'cancel' }, { text: 'Reboot', style: 'destructive', onPress: async () => { try { await api.rebootDevice(device.id); Burnt.toast({ title: 'Success', preset: 'done', message: `Rebooting ${device.name}.`, }); } catch (error: any) { Burnt.toast({ title: 'Error', preset: 'error', message: error.message || `Failed to reboot ${device.name}`, }); } }, }, ]); }; const handleShutdown = async (device: Device) => { Alert.alert( 'Confirm Shutdown', `Shutdown ${device.name}? This cannot be undone.`, [ { text: 'Cancel', style: 'cancel' }, { text: 'Shutdown', style: 'destructive', onPress: async () => { try { await api.shutdownDevice(device.id); Burnt.toast({ title: 'Success', preset: 'done', message: `Shutting down ${device.name}.`, }); } catch (error: any) { Burnt.toast({ title: 'Error', preset: 'error', message: error.message || `Failed to shut down ${device.name}.`, }); } }, }, ] ); }; const handleDelete = (device: Device) => { Alert.alert('Delete Device', `Delete "${device.name}"?`, [ { text: 'Cancel', style: 'cancel', onPress: () => { // Close alert }, }, { text: 'Delete', style: 'destructive', onPress: async () => { try { await api.deleteDevice(device.id); Burnt.toast({ title: 'Success', preset: 'done', message: `Deleted ${device.name} successfully.`, }); fetchDevices(false); } catch (error: any) { Burnt.toast({ title: 'Error', preset: 'error', message: error.message || `Failed to delete ${device.name}.`, }); } }, }, ]); }; const getStatusColor = (status: string) => { switch (status?.toLowerCase()) { case 'online': return '#4CAF50'; case 'offline': return '#f44336'; default: return '#ff9800'; } }; const ActionIcon = ({ name, symbolName, color, onPress, fallbackName, }: { name: string; symbolName: string; color: string; onPress: () => void; fallbackName?: string; }) => ( } style={styles.actionIcon} /> ); const renderDevice = ({ item }: { item: Device }) => ( handleDelete(item)} > Delete Device {item.name} {item.mac} } style={[ styles.statusSymbol, { shadowColor: getStatusColor(item.status), shadowOpacity: isDark ? 0.9 : 0.6, }, ]} /> {item.status?.toLowerCase() === 'offline' && ( handleWake(item)} /> )} {item.status?.toLowerCase() === 'online' && ( <> handleSleep(item)} /> handleReboot(item)} /> handleShutdown(item)} /> )} ); if (isLoading) { return ( ); } return ( item.id} contentContainerStyle={styles.list} refreshControl={ } ListEmptyComponent={ No devices found } /> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#f5f5f5', }, headerRight: { flexDirection: 'row', gap: 16, }, headerButton: { paddingHorizontal: 8, }, loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', }, list: { padding: 15, gap: 15, }, deviceCard: { backgroundColor: '#fff', borderRadius: 12, padding: 15, paddingRight: 25, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 3, }, deviceHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, }, deviceInfo: { flex: 1, }, deviceName: { fontSize: 18, fontWeight: 'bold', color: '#333', marginBottom: 4, }, deviceIP: { fontSize: 14, color: '#666', }, statusDot: { width: 12, height: 12, borderRadius: 6, }, deviceActions: { flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-start', marginTop: 8, paddingBottom: 6, }, actionIconContainer: { padding: 6, marginRight: 12, justifyContent: 'center', alignItems: 'center', minWidth: 36, minHeight: 36, }, actionIcon: { width: 22, height: 22, }, actionButtonText: { color: '#fff', fontWeight: '600', fontSize: 12, }, wakeButton: { backgroundColor: '#4CAF50', }, sleepButton: { backgroundColor: '#FF9800', }, rebootButton: { backgroundColor: '#2196F3', }, shutdownButton: { backgroundColor: '#f44336', }, statusSymbol: { width: 18, height: 18, borderRadius: 9, shadowOffset: { width: 0, height: 0 }, shadowOpacity: 0.6, shadowRadius: 8, elevation: 6, }, emptyContainer: { alignItems: 'center', paddingVertical: 50, }, emptyText: { fontSize: 16, color: '#999', }, });