import { Button, ContextMenu, Host, } from '@expo/ui/swift-ui'; import { Ionicons } from '@expo/vector-icons'; import * as Burnt from 'burnt'; 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 { syncDevicesToWidget } from '../../src/services/widgetSync'; import { Device } from '../../src/types'; const isAuthError = (error: unknown) => typeof error === 'object' && error !== null && 'isAuthError' in error && (error as { isAuthError?: boolean }).isAuthError === true; 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); // Sync devices to iOS widget syncDevicesToWidget(data); } catch (error: any) { if (isAuthError(error)) { setDevices([]); return; } // 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 }) => { const isOnline = item.status?.toLowerCase() === 'online'; const isOffline = item.status?.toLowerCase() === 'offline'; const hasActions = isOnline || isOffline; return (