diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..46be414 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{js,jsx,ts,tsx,json,css,scss,md}] +indent_size = 2 diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..2781659 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": true, + "trailingComma": "es5", + "arrowParens": "avoid" +} diff --git a/.vscode/settings.json b/.vscode/settings.json index e2798e4..c5614fc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,16 @@ "source.fixAll": "explicit", "source.organizeImports": "explicit", "source.sortMembers": "explicit" + }, + "editor.detectIndentation": false, + "editor.tabSize": 2, + "editor.insertSpaces": true, + "[javascript]": { + "editor.tabSize": 2, + "editor.insertSpaces": true + }, + "[typescript]": { + "editor.tabSize": 2, + "editor.insertSpaces": true } } diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 5321b75..e1a2475 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -1,509 +1,510 @@ -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 { 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"; + 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 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 [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); - } - }, []); + 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(() => { + fetchDevices(true); + }, [fetchDevices]); - useEffect(() => { - let intervalId: number | null = null; + useEffect(() => { + let intervalId: number | null = null; - const startPolling = () => { - if (intervalId !== null) return; - intervalId = setInterval(() => { - fetchDevices(false); - }, 10000) as unknown as number; - }; + const startPolling = () => { + if (intervalId !== null) return; + intervalId = setInterval(() => { + fetchDevices(false); + }, 10000) as unknown as number; + }; - const stopPolling = () => { - if (intervalId !== null) { - clearInterval(intervalId); - intervalId = null; - } - }; + const stopPolling = () => { + if (intervalId !== null) { + clearInterval(intervalId); + intervalId = null; + } + }; - // Start polling while app is active; pause when backgrounded - startPolling(); + // Start polling while app is active; pause when backgrounded + startPolling(); - const onAppStateChange = (nextAppState: string) => { - if (nextAppState === "active") { - startPolling(); - } else { - stopPolling(); - } - }; + const onAppStateChange = (nextAppState: string) => { + if (nextAppState === 'active') { + startPolling(); + } else { + stopPolling(); + } + }; - const subscription = AppState.addEventListener("change", onAppStateChange); + const subscription = AppState.addEventListener('change', onAppStateChange); - return () => { - stopPolling(); - subscription.remove(); - }; - }, [fetchDevices]); + return () => { + stopPolling(); + subscription.remove(); + }; + }, [fetchDevices]); - const onRefresh = async () => { - setRefreshing(true); - await fetchDevices(false); - setRefreshing(false); - }; + const onRefresh = async () => { + setRefreshing(true); + await fetchDevices(false); + setRefreshing(false); + }; - const handleWake = async (device: Device) => { - try { - await api.wakeDevice(device.id); + const handleWake = async (device: Device) => { + try { + await api.wakeDevice(device.id); Burnt.toast({ - title: "Success", - preset: "done", + title: 'Success', + preset: 'done', message: `Waking ${device.name} up.`, }); - } catch (error: any) { + } catch (error: any) { Burnt.toast({ - title: "Error", - preset: "error", + 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", + 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.`, + } 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", + 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", + } 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); + 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", + title: 'Success', + preset: 'done', message: `Shutting down ${device.name}.`, }); - } catch (error: any) { + } catch (error: any) { Burnt.toast({ - title: "Error", - preset: "error", + 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); + 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", + title: 'Success', + preset: 'done', message: `Deleted ${device.name} successfully.`, }); - fetchDevices(false); - } catch (error: any) { + fetchDevices(false); + } catch (error: any) { Burnt.toast({ - title: "Error", - preset: "error", + 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 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 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, - }, - ]} - /> - + 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, + }, + ]} + /> + - - handleWake(item)} - /> - handleSleep(item)} - /> - handleReboot(item)} - /> - handleShutdown(item)} - /> - - - - - - - ); + + handleWake(item)} + /> + handleSleep(item)} + /> + handleReboot(item)} + /> + handleShutdown(item)} + /> + + + + + + + ); - if (isLoading) { - return ( - - - - ); - } + if (isLoading) { + return ( + + + + ); + } - return ( - - item.id} - contentContainerStyle={styles.list} - refreshControl={ - - } - ListEmptyComponent={ - - - No devices found - - - } - /> - - ); + 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", - }, + 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', + }, });