diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0c86b2d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Joshua Higgins + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 76b1807..0343f01 100644 --- a/README.md +++ b/README.md @@ -17,24 +17,23 @@ A React Native Expo app that connects to an UpSnap server and provides mobile ac - Node.js installed - Expo CLI installed (`npm install -g expo-cli`) -- An UpSnap server instance running (e.g., https://wol.f6knight.duckdns.org/) +- An UpSnap server instance running ### Installation 1. Install dependencies: ```bash -npm install +npm i ``` 2. Start the development server: ```bash -npm start +npx expo run ``` 3. Run on your preferred platform: - - iOS: Press `i` in the terminal or run `npm run ios` - - Android: Press `a` in the terminal or run `npm run android` - - Web: Press `w` in the terminal or run `npm run web` + - iOS: Press `i` in the terminal or run `npx expo run:ios` + - Android: Press `a` in the terminal or run `npx expo run:android` ## Usage @@ -42,25 +41,15 @@ npm start 1. Open the app 2. Enter your UpSnap server credentials: + - Server Address - Username or Email - Password - - Check "Login as Admin" if you're an admin user 3. Tap "Login" ### Device Management #### Adding a Device -1. Tap the "+ Add" button in the Devices screen -2. Fill in the required fields: - - Device Name (e.g., "My PC") - - MAC Address (format: XX:XX:XX:XX:XX:XX) - - IP Address - - Optional: Netmask, Broadcast Address, SecureOn Password, Port -3. Tap "Add Device" - -#### Scanning for Devices - 1. Tap the "Scan" button in the Devices screen 2. Wait for the network scan to complete 3. Tap on a discovered device to add it to your list @@ -68,7 +57,6 @@ npm start #### Managing Devices - **View Devices**: Scroll through the list on the main screen -- **Device Details**: Tap on a device card to view and edit details - **Wake Device**: Tap the green "Wake" button on a device card - **Sleep Device**: Tap the orange "Sleep" button - **Reboot Device**: Tap the blue "Reboot" button @@ -80,93 +68,8 @@ npm start - 🔴 Red dot: Device is offline - 🟠 Orange dot: Status unknown -## Screens - -### Login Screen -- Username/email and password fields -- Admin login option -- Persistent authentication using AsyncStorage - -### Devices Screen -- List of all devices with status indicators -- Quick action buttons for each device -- Pull-to-refresh for updating status -- Add device and scan network buttons - -### Device Details Screen -- View all device information -- Edit device details -- Delete device option - -### Add Device Screen -- Manual device entry form -- All required and optional fields -- Input validation - -### Scan Devices Screen -- Network scanning functionality -- List of discovered devices -- Quick add to device list - -### Settings Screen -- User information display -- Server configuration -- Logout option -- Clear data option - -## API Integration - -The app connects to an UpSnap server via its REST API: - -- **Authentication**: `/api/collections/users/auth-with-password` or `/api/collections/_superusers/auth-with-password` -- **Devices**: `/api/collections/devices/records` -- **Wake**: `/api/upsnap/wake/:id` -- **Sleep**: `/api/collections/upsnap/sleep/:id` -- **Reboot**: `/api/upsnap/reboot/:id` -- **Shutdown**: `/api/upsnap/shutdown/:id` -- **Scan**: `/api/upsnap/scan` - -## Project Structure - -``` -src/ -├── components/ # Reusable components -├── context/ # React context providers (AuthContext) -├── navigation/ # Navigation setup -├── screens/ # All app screens -│ ├── LoginScreen.tsx -│ ├── DeviceListScreen.tsx -│ ├── DeviceDetailsScreen.tsx -│ ├── AddDeviceScreen.tsx -│ ├── ScanDevicesScreen.tsx -│ └── SettingsScreen.tsx -├── services/ # API service -│ └── api.ts -└── types/ # TypeScript type definitions - └── index.ts -``` - -## Tech Stack - -- **React Native**: Cross-platform mobile framework -- **Expo**: Development platform -- **TypeScript**: Type safety -- **React Navigation**: Navigation library -- **AsyncStorage**: Local data persistence - -## Server Configuration - -The app is pre-configured to connect to: -- Server URL: `https://wol.f6knight.duckdns.org` -- API Base: `/api` - -To change the server URL, modify the `API_BASE_URL` constant in `src/services/api.ts`. - ## Security Notes -- Authentication tokens are stored securely in AsyncStorage -- All API calls require authentication -- Destructive actions (sleep, reboot, shutdown) require confirmation - Never expose your UpSnap server to the open web without proper security measures ## Troubleshooting @@ -174,7 +77,6 @@ To change the server URL, modify the `API_BASE_URL` constant in `src/services/ap ### Login Issues - Verify your username/email and password are correct -- Check if you need to log in as admin - Ensure your UpSnap server is accessible ### Network Scan Issues @@ -192,7 +94,7 @@ To change the server URL, modify the `API_BASE_URL` constant in `src/services/ap ## License -This project is a mobile companion to UpSnap, which is licensed under the MIT License. +This project is licensed under the MIT License. ## Credits diff --git a/app.json b/app.json index fca5712..34426dd 100644 --- a/app.json +++ b/app.json @@ -1,6 +1,6 @@ { "expo": { - "name": "remote-wol", + "name": "Remote WoL", "slug": "remote-wol", "version": "1.0.0", "orientation": "portrait", @@ -10,7 +10,8 @@ "newArchEnabled": true, "ios": { "supportsTablet": true, - "bundleIdentifier": "com.anonymous.remote-wol" + "icon": "./assets/remotewol-ios.icon", + "bundleIdentifier": "com.abunchofknowitalls.remotewol-upsnap" }, "android": { "adaptiveIcon": { diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 4ee0119..b72288a 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -1,11 +1,14 @@ -import React from "react"; import { Ionicons } from "@expo/vector-icons"; import { useRouter } from "expo-router"; -import { View, TouchableOpacity, StyleSheet } from "react-native"; -import { NativeTabs, Icon, Label } from "expo-router/unstable-native-tabs"; +import { Icon, Label, NativeTabs } from "expo-router/unstable-native-tabs"; +import React from "react"; +import { StyleSheet, TouchableOpacity, View } from "react-native"; +import { useColorScheme } from "../../hooks/use-color-scheme"; export function DevicesHeader() { const router = useRouter(); + const isDark = useColorScheme() === "dark"; + const activityColor = isDark ? "#0A84FF" : "#007AFF"; return ( @@ -13,7 +16,7 @@ export function DevicesHeader() { onPress={() => router.push("/scan-devices")} style={styles.headerButton} > - + ); @@ -23,12 +26,12 @@ export default function TabsLayout() { return ( - - + + - - + + ); diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 222a990..5321b75 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -1,66 +1,111 @@ -import React, { useState, useEffect } from "react"; -import { - View, - Text, - FlatList, - TouchableOpacity, - StyleSheet, - RefreshControl, - Alert, - ActivityIndicator, -} from "react-native"; -import { ContextMenu, Button as SwiftUIButton, Host } from "@expo/ui/swift-ui"; -import { useRouter, useLocalSearchParams, Stack } from "expo-router"; +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"; - -function DevicesHeader() { - const router = useRouter(); - - return ( - - router.push("/scan-devices")}> - - - - ); -} +import * as Burnt from "burnt"; export default function DeviceListScreen() { - const router = useRouter(); + 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); - useEffect(() => { - loadDevices(); - }, []); - - const loadDevices = async () => { + const fetchDevices = useCallback(async (showLoading = false) => { try { - setIsLoading(true); + if (showLoading) setIsLoading(true); const data = await api.getDevices(); setDevices(data); } catch (error: any) { - Alert.alert("Error", error.message || "Failed to load devices"); + // For background/periodic refreshes, avoid interruptive alerts + if (showLoading) { + Alert.alert("Error", error.message || "Failed to load devices"); + } } finally { - setIsLoading(false); + 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 loadDevices(); + await fetchDevices(false); setRefreshing(false); }; const handleWake = async (device: Device) => { try { await api.wakeDevice(device.id); - Alert.alert("Success", `Wake signal sent to ${device.name}`); + Burnt.toast({ + title: "Success", + preset: "done", + message: `Waking ${device.name} up.`, + }); } catch (error: any) { - Alert.alert("Error", error.message || "Failed to wake device"); + Burnt.toast({ + title: "Error", + preset: "error", + message: error.message || `Failed to wake up ${device.name}.`, + }); } }; @@ -73,9 +118,17 @@ export default function DeviceListScreen() { onPress: async () => { try { await api.sleepDevice(device.id); - Alert.alert("Success", `Sleep signal sent to ${device.name}`); + Burnt.toast({ + title: "Success", + preset: "done", + message: `Sending ${device.name} to sleep.`, + }); } catch (error: any) { - Alert.alert("Error", error.message || "Failed to sleep device"); + Burnt.toast({ + title: "Error", + preset: "error", + message: error.message || `Failed to send ${device.name} to sleep.`, + }); } }, }, @@ -91,9 +144,17 @@ export default function DeviceListScreen() { onPress: async () => { try { await api.rebootDevice(device.id); - Alert.alert("Success", `Reboot signal sent to ${device.name}`); + Burnt.toast({ + title: "Success", + preset: "done", + message: `Rebooting ${device.name}.`, + }); } catch (error: any) { - Alert.alert("Error", error.message || "Failed to reboot device"); + Burnt.toast({ + title: "Error", + preset: "error", + message: error.message || `Failed to reboot ${device.name}`, + }); } }, }, @@ -112,12 +173,17 @@ export default function DeviceListScreen() { onPress: async () => { try { await api.shutdownDevice(device.id); - Alert.alert("Success", `Shutdown signal sent to ${device.name}`); + Burnt.toast({ + title: "Success", + preset: "done", + message: `Shutting down ${device.name}.`, + }); } catch (error: any) { - Alert.alert( - "Error", - error.message || "Failed to shutdown device" - ); + Burnt.toast({ + title: "Error", + preset: "error", + message: error.message || `Failed to shut down ${device.name}.`, + }); } }, }, @@ -127,16 +193,31 @@ export default function DeviceListScreen() { 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); - Alert.alert("Success", "Device deleted successfully"); - loadDevices(); + Burnt.toast({ + title: "Success", + preset: "done", + message: `Deleted ${device.name} successfully.`, + }); + fetchDevices(false); } catch (error: any) { - Alert.alert("Error", error.message || "Failed to delete device"); + Burnt.toast({ + title: "Error", + preset: "error", + message: error.message || `Failed to delete ${device.name}.`, + }); } }, }, @@ -154,6 +235,42 @@ export default function DeviceListScreen() { } }; + 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 }) => ( @@ -167,51 +284,83 @@ export default function DeviceListScreen() { - - router.push(`/devices/${item.id}`)} - activeOpacity={1} - > + + - {item.name} - {item.ip} + + {item.name} + + + {item.mac} + - + } style={[ - styles.statusDot, - { backgroundColor: getStatusColor(item.status) }, + styles.statusSymbol, + { + shadowColor: getStatusColor(item.status), + shadowOpacity: isDark ? 0.9 : 0.6, + }, ]} /> - handleWake(item)} - > - Wake - - + handleSleep(item)} - > - Sleep - - + handleReboot(item)} - > - Reboot - - + handleShutdown(item)} - > - Shutdown - + /> - + @@ -221,19 +370,13 @@ export default function DeviceListScreen() { if (isLoading) { return ( - + ); } return ( - - , - }} - /> + - No devices found + + No devices found + } /> @@ -271,12 +416,13 @@ const styles = StyleSheet.create({ }, list: { padding: 15, + gap: 15, }, deviceCard: { backgroundColor: "#fff", borderRadius: 12, padding: 15, - marginBottom: 15, + paddingRight: 25, shadowColor: "#000", shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, @@ -309,14 +455,27 @@ const styles = StyleSheet.create({ }, deviceActions: { flexDirection: "row", - justifyContent: "space-between", - gap: 8, - }, - actionButton: { - flex: 1, - paddingVertical: 10, - borderRadius: 8, 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", @@ -330,10 +489,14 @@ const styles = StyleSheet.create({ shutdownButton: { backgroundColor: "#f44336", }, - actionButtonText: { - color: "#fff", - fontWeight: "600", - fontSize: 12, + statusSymbol: { + width: 18, + height: 18, + borderRadius: 9, + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0.6, + shadowRadius: 8, + elevation: 6, }, emptyContainer: { alignItems: "center", diff --git a/app/(tabs)/settings.tsx b/app/(tabs)/settings.tsx index d163619..f5e5e3e 100644 --- a/app/(tabs)/settings.tsx +++ b/app/(tabs)/settings.tsx @@ -1,231 +1,251 @@ -import React from 'react'; +import AsyncStorage from "@react-native-async-storage/async-storage"; +import React from "react"; import { - View, - Text, - StyleSheet, - ScrollView, - TouchableOpacity, - Alert, -} from 'react-native'; -import { useAuth } from '../../src/context/AuthContext'; -import AsyncStorage from '@react-native-async-storage/async-storage'; + Alert, + ScrollView, + StyleSheet, + Text, + TouchableOpacity, + View, +} from "react-native"; +import { useColorScheme } from "../../hooks/use-color-scheme"; +import { useAuth } from "../../src/context/AuthContext"; +import { useRouter } from "expo-router"; export default function SettingsScreen() { - const { user, logout } = useAuth(); + const { user, serverAddress, logout } = useAuth(); + const router = useRouter(); + const colorScheme = useColorScheme() ?? "light"; + const isDark = colorScheme === "dark"; + const bgColor = isDark ? "#0b0b0d" : "#f5f5f5"; + const sectionBg = isDark ? "#1c1c1e" : "#fff"; + const sectionTitleBg = isDark ? "#111111" : "#f9f9f9"; + const textColor = isDark ? "#fff" : "#333"; + const subTextColor = isDark ? "#c6c6c8" : "#666"; + const primary = isDark ? "#0A84FF" : "#007AFF"; + const destructiveColor = "#f44336"; - const handleLogout = () => { - Alert.alert( - 'Logout', - 'Are you sure you want to logout?', - [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Logout', - style: 'destructive', - onPress: async () => { - await logout(); - }, - }, - ] - ); - }; + const handleLogout = () => { + Alert.alert("Logout", "Are you sure you want to logout?", [ + { text: "Cancel", style: "cancel" }, + { + text: "Logout", + style: "destructive", + onPress: async () => { + router.replace("/login"); + await logout(); + }, + }, + ]); + }; - const handleClearData = () => { - Alert.alert( - 'Clear Data', - 'This will clear all stored data. Are you sure?', - [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Clear', - style: 'destructive', - onPress: async () => { - try { - await AsyncStorage.clear(); - Alert.alert('Success', 'All data cleared'); - } catch (error) { - Alert.alert('Error', 'Failed to clear data'); - } - }, - }, - ] - ); - }; + const SettingItem = ({ + label, + value, + onPress, + }: { + label: string; + value?: string; + onPress?: () => void; + }) => ( + + {label} + + + {value} + + + + ); - const SettingItem = ({ - label, - value, - onPress - }: { - label: string; - value?: string; - onPress?: () => void; - }) => ( - - {label} - - - {value || 'N/A'} - - - - ); + const ActionButton = ({ + title, + onPress, + destructive = false, + }: { + title: string; + onPress: () => void; + destructive?: boolean; + }) => ( + + {title} + + ); - const ActionButton = ({ - title, - onPress, - destructive = false, - }: { - title: string; - onPress: () => void; - destructive?: boolean; - }) => ( - - - {title} - - - ); + return ( + + + + + User Information + + + + + - return ( - - - - User Information - - - - + + + App Info + + + + - - Server - - - + + + Actions + + + - - App Info - - - - - - Actions - - - - - - - UpSnap Mobile App - - - Connect to your UpSnap server - - - - - ); + + + UpSnap Mobile App + + + Connect to your UpSnap server + + + + + ); } const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - content: { - padding: 20, - }, - section: { - backgroundColor: '#fff', - borderRadius: 12, - marginBottom: 20, - overflow: 'hidden', - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 3, - }, - sectionTitle: { - fontSize: 14, - fontWeight: 'bold', - color: '#666', - paddingHorizontal: 15, - paddingTop: 15, - paddingBottom: 10, - backgroundColor: '#f9f9f9', - }, - settingItem: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - paddingHorizontal: 15, - paddingVertical: 15, - borderBottomWidth: 1, - borderBottomColor: '#f0f0f0', - }, - settingLabel: { - fontSize: 16, - color: '#333', - flex: 1, - }, - settingValueContainer: { - flex: 1, - alignItems: 'flex-end', - }, - settingValue: { - fontSize: 14, - color: '#666', - maxWidth: 200, - }, - actionButton: { - backgroundColor: '#007AFF', - margin: 15, - padding: 15, - borderRadius: 8, - alignItems: 'center', - }, - actionButtonDestructive: { - backgroundColor: '#f44336', - }, - actionButtonText: { - color: '#fff', - fontSize: 16, - fontWeight: 'bold', - }, - actionButtonTextDestructive: { - color: '#fff', - }, - footer: { - alignItems: 'center', - paddingVertical: 30, - }, - footerText: { - fontSize: 14, - color: '#999', - marginBottom: 5, - }, + container: { + flex: 1, + backgroundColor: "#f5f5f5", + }, + content: { + padding: 20, + }, + section: { + backgroundColor: "#fff", + borderRadius: 12, + marginBottom: 20, + overflow: "hidden", + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, + }, + sectionTitle: { + fontSize: 14, + fontWeight: "bold", + color: "#666", + paddingHorizontal: 15, + paddingTop: 15, + paddingBottom: 10, + backgroundColor: "#f9f9f9", + borderTopLeftRadius: 12, + borderTopRightRadius: 12, + }, + settingItem: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingHorizontal: 15, + paddingVertical: 15, + borderBottomWidth: 1, + borderBottomColor: "#f0f0f0", + }, + settingLabel: { + fontSize: 16, + color: "#333", + flex: 1, + }, + settingValueContainer: { + flex: 1, + alignItems: "flex-end", + }, + settingValue: { + fontSize: 14, + color: "#666", + maxWidth: 200, + }, + actionButton: { + backgroundColor: "#007AFF", + margin: 15, + padding: 15, + borderRadius: 8, + alignItems: "center", + }, + actionButtonDestructive: { + backgroundColor: "#f44336", + }, + actionButtonText: { + color: "#fff", + fontSize: 16, + fontWeight: "bold", + }, + actionButtonTextDestructive: { + color: "#fff", + }, + footer: { + alignItems: "center", + paddingVertical: 30, + }, + footerText: { + fontSize: 14, + color: "#999", + marginBottom: 5, + }, }); diff --git a/app/_layout.tsx b/app/_layout.tsx index 09b27ff..6bf398e 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,41 +1,77 @@ -import { StatusBar } from "expo-status-bar"; -import { AuthProvider, useAuth } from "../src/context/AuthContext"; -import { useEffect } from "react"; -import { Stack, useRouter } from "expo-router"; +import { Ionicons } from "@expo/vector-icons"; +import { Stack, useRouter, useSegments } from "expo-router"; +import { TouchableOpacity, Text } from "react-native"; +import { useColorScheme } from "../hooks/use-color-scheme"; +import { AuthProvider } from "../src/context/AuthContext"; -function RootStack() { - const router = useRouter(); - const { isAuthenticated, isLoading } = useAuth(); - - useEffect(() => { - if (!isLoading) { - if (isAuthenticated) { - router.replace('/(tabs)'); - } else { - router.replace('/login'); - } - } - }, [isAuthenticated, isLoading, router]); +function DevicesHeader() { + const router = useRouter(); + const isDark = useColorScheme() === "dark"; + const activityColor = isDark ? "#0A84FF" : "#007AFF"; return ( - - {isAuthenticated ? ( - - ) : ( - - )} - - - - + router.push("/scan-devices")} + style={{ paddingHorizontal: 8 }} + > + + + ); +} + +function TabsTitle(props: { title: string }) { + const segments = useSegments(); + const last = segments[segments.length - 1]; + const isDark = useColorScheme() === "dark"; + const titleColor = isDark ? "#fff" : "#000"; + const title = last === "settings" ? "Settings" : props.title; + return ( + + {title} + ); } export default function RootLayout() { + const isDark = useColorScheme() === "dark"; + const bgColor = isDark ? "#0b0b0d" : "#fff"; + const titleColor = isDark ? "#fff" : "#000"; + + const segments = useSegments(); + const last = segments[segments.length - 1]; + const isIndex = last === "(tabs)" || last === undefined; + return ( - - + + {/* Root index that performs auth redirect (app/index.tsx) */} + + {/* Tabs parent - render tabs with dynamic header */} + , + headerRight: isIndex ? () => : undefined, + headerRightContainerStyle: isIndex + ? undefined + : { width: 0, paddingRight: 0 }, + headerStyle: { backgroundColor: bgColor }, + headerTintColor: titleColor, + } as any + } + />{" "} + + , + headerStyle: { backgroundColor: bgColor }, + headerBackButtonDisplayMode: "minimal", + }} + /> + ); } diff --git a/app/add-device.tsx b/app/add-device.tsx deleted file mode 100644 index 21c3d75..0000000 --- a/app/add-device.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import React, { useState } from 'react'; -import { - View, - Text, - ScrollView, - StyleSheet, - TextInput, - TouchableOpacity, - Alert, - ActivityIndicator, -} from 'react-native'; -import { useRouter } from 'expo-router'; -import api from '../src/services/api'; - -export default function AddDeviceScreen() { - const router = useRouter(); - const [isLoading, setIsLoading] = useState(false); - - const [formData, setFormData] = useState({ - name: '', - mac: '', - ip: '', - netmask: '255.255.255.0', - broadcast: '', - secureOnPassword: '', - port: '9', - }); - - const handleSave = async () => { - if (!formData.name || !formData.mac || !formData.ip) { - Alert.alert('Error', 'Please fill in all required fields'); - return; - } - - setIsLoading(true); - try { - await api.createDevice({ - name: formData.name, - mac: formData.mac, - ip: formData.ip, - netmask: formData.netmask, - broadcast: formData.broadcast, - secureOnPassword: formData.secureOnPassword, - port: parseInt(formData.port) || 9, - groups: [], - status: 'offline', - }); - Alert.alert('Success', 'Device added successfully'); - router.back(); - } catch (error: any) { - Alert.alert('Error', error.message || 'Failed to add device'); - } finally { - setIsLoading(false); - } - }; - - return ( - - - Add New Device - - - - Device Name * - setFormData({ ...formData, name: text })} - placeholder="My PC" - autoCapitalize="words" - /> - - - - MAC Address * - setFormData({ ...formData, mac: text })} - placeholder="00:11:22:33:44:55" - autoCapitalize="characters" - /> - - Format: XX:XX:XX:XX:XX:XX - - - - - IP Address * - setFormData({ ...formData, ip: text })} - placeholder="192.168.1.100" - keyboardType="numeric" - /> - - - - Netmask - setFormData({ ...formData, netmask: text })} - placeholder="255.255.255.0" - keyboardType="numeric" - /> - - - - Broadcast Address - setFormData({ ...formData, broadcast: text })} - placeholder="192.168.1.255" - keyboardType="numeric" - /> - - Optional: Auto-calculated if left blank - - - - - SecureOn Password - setFormData({ ...formData, secureOnPassword: text })} - placeholder="Optional password" - secureTextEntry - /> - - Optional: For SecureOn enabled NICs - - - - - Port - setFormData({ ...formData, port: text })} - placeholder="9" - keyboardType="numeric" - /> - - Default: 9 (Standard Wake-on-LAN port) - - - - - router.back()} - disabled={isLoading} - > - Cancel - - - - {isLoading ? 'Adding...' : 'Add Device'} - - - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - content: { - padding: 20, - }, - header: { - fontSize: 24, - fontWeight: 'bold', - color: '#333', - marginBottom: 20, - textAlign: 'center', - }, - form: { - gap: 20, - }, - inputGroup: { - gap: 8, - }, - label: { - fontSize: 14, - fontWeight: '600', - color: '#333', - }, - input: { - backgroundColor: '#fff', - borderWidth: 1, - borderColor: '#ddd', - borderRadius: 8, - padding: 12, - fontSize: 16, - }, - hint: { - fontSize: 12, - color: '#666', - }, - buttonGroup: { - flexDirection: 'row', - gap: 10, - marginTop: 10, - }, - button: { - flex: 1, - paddingVertical: 15, - borderRadius: 8, - alignItems: 'center', - }, - cancelButton: { - backgroundColor: '#999', - }, - saveButton: { - backgroundColor: '#4CAF50', - }, - buttonText: { - color: '#fff', - fontSize: 16, - fontWeight: 'bold', - }, -}); diff --git a/app/devices/[id].tsx b/app/devices/[id].tsx deleted file mode 100644 index 4e11a41..0000000 --- a/app/devices/[id].tsx +++ /dev/null @@ -1,350 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { - View, - Text, - ScrollView, - StyleSheet, - TextInput, - TouchableOpacity, - Alert, - ActivityIndicator, -} from 'react-native'; -import { useLocalSearchParams, useRouter } from 'expo-router'; -import api from '../../src/services/api'; -import { Device } from '../../src/types'; - -export default function DeviceDetailsScreen() { - const router = useRouter(); - const { id: deviceId } = useLocalSearchParams<{ id: string }>(); - - const [device, setDevice] = useState(null); - const [isEditing, setIsEditing] = useState(false); - const [isLoading, setIsLoading] = useState(true); - const [isSaving, setIsSaving] = useState(false); - - const [formData, setFormData] = useState({ - name: '', - mac: '', - ip: '', - netmask: '', - broadcast: '', - secureOnPassword: '', - port: '', - }); - - useEffect(() => { - loadDevice(); - }, [deviceId]); - - const loadDevice = async () => { - try { - setIsLoading(true); - const data = await api.getDevice(deviceId); - setDevice(data); - setFormData({ - name: data.name, - mac: data.mac, - ip: data.ip, - netmask: data.netmask || '', - broadcast: data.broadcast || '', - secureOnPassword: data.secureOnPassword || '', - port: String(data.port), - }); - } catch (error: any) { - Alert.alert('Error', error.message || 'Failed to load device'); - router.back(); - } finally { - setIsLoading(false); - } - }; - - const handleSave = async () => { - if (!formData.name || !formData.mac || !formData.ip) { - Alert.alert('Error', 'Please fill in all required fields'); - return; - } - - setIsSaving(true); - try { - await api.updateDevice(deviceId, { - name: formData.name, - mac: formData.mac, - ip: formData.ip, - netmask: formData.netmask, - broadcast: formData.broadcast, - secureOnPassword: formData.secureOnPassword, - port: parseInt(formData.port) || 9, - }); - setIsEditing(false); - await loadDevice(); - Alert.alert('Success', 'Device updated successfully'); - } catch (error: any) { - Alert.alert('Error', error.message || 'Failed to update device'); - } finally { - setIsSaving(false); - } - }; - - const handleDelete = () => { - Alert.alert( - 'Delete Device', - 'Are you sure you want to delete this device?', - [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Delete', - style: 'destructive', - onPress: async () => { - try { - await api.deleteDevice(deviceId); - Alert.alert('Success', 'Device deleted successfully'); - router.back(); - } catch (error: any) { - Alert.alert('Error', error.message || 'Failed to delete device'); - } - }, - }, - ] - ); - }; - - if (isLoading) { - return ( - - - - ); - } - - return ( - - - {isEditing ? ( - - Device Name * - setFormData({ ...formData, name: text })} - placeholder="Device name" - /> - - MAC Address * - setFormData({ ...formData, mac: text })} - placeholder="00:11:22:33:44:55" - autoCapitalize="characters" - /> - - IP Address * - setFormData({ ...formData, ip: text })} - placeholder="192.168.1.100" - keyboardType="numeric" - /> - - Netmask - setFormData({ ...formData, netmask: text })} - placeholder="255.255.255.0" - keyboardType="numeric" - /> - - Broadcast Address - setFormData({ ...formData, broadcast: text })} - placeholder="192.168.1.255" - keyboardType="numeric" - /> - - SecureOn Password - setFormData({ ...formData, secureOnPassword: text })} - placeholder="Optional password" - /> - - Port - setFormData({ ...formData, port: text })} - placeholder="9" - keyboardType="numeric" - /> - - - - - {isSaving ? 'Saving...' : 'Save'} - - - { - setIsEditing(false); - setFormData({ - name: device!.name, - mac: device!.mac, - ip: device!.ip, - netmask: device!.netmask || '', - broadcast: device!.broadcast || '', - secureOnPassword: device!.secureOnPassword || '', - port: String(device!.port), - }); - }} - > - Cancel - - - - ) : ( - - - Name - {device?.name} - - - MAC Address - {device?.mac} - - - IP Address - {device?.ip} - - - Netmask - {device?.netmask || 'N/A'} - - - Broadcast - {device?.broadcast || 'N/A'} - - - Port - {device?.port} - - - Status - {device?.status} - - - Groups - - {device?.groups?.join(', ') || 'None'} - - - - - setIsEditing(true)} - > - Edit - - - Delete - - - - )} - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - loadingContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - content: { - padding: 20, - }, - form: { - gap: 15, - }, - details: { - gap: 20, - }, - label: { - fontSize: 14, - fontWeight: '600', - color: '#333', - marginBottom: 5, - }, - input: { - backgroundColor: '#fff', - borderWidth: 1, - borderColor: '#ddd', - borderRadius: 8, - padding: 12, - fontSize: 16, - }, - detailRow: { - backgroundColor: '#fff', - borderRadius: 8, - padding: 15, - borderWidth: 1, - borderColor: '#e0e0e0', - }, - detailLabel: { - fontSize: 14, - color: '#666', - marginBottom: 4, - }, - detailValue: { - fontSize: 16, - color: '#333', - fontWeight: '500', - }, - buttonGroup: { - flexDirection: 'row', - gap: 10, - marginTop: 20, - }, - button: { - flex: 1, - paddingVertical: 15, - borderRadius: 8, - alignItems: 'center', - }, - saveButton: { - backgroundColor: '#4CAF50', - }, - cancelButton: { - backgroundColor: '#999', - }, - editButton: { - backgroundColor: '#007AFF', - }, - deleteButton: { - backgroundColor: '#f44336', - }, - buttonText: { - color: '#fff', - fontSize: 16, - fontWeight: 'bold', - }, -}); diff --git a/app/index.tsx b/app/index.tsx new file mode 100644 index 0000000..e3390d1 --- /dev/null +++ b/app/index.tsx @@ -0,0 +1,21 @@ +import { useRouter } from "expo-router"; +import { StatusBar } from "expo-status-bar"; +import React, { useEffect } from "react"; +import { useAuth } from "../src/context/AuthContext"; + +export default function IndexRedirect() { + const router = useRouter(); + const { isAuthenticated, isLoading } = useAuth(); + + useEffect(() => { + if (!isLoading) { + if (isAuthenticated) { + router.replace("/(tabs)"); + } else { + router.replace("/login"); + } + } + }, [isAuthenticated, isLoading, router]); + + return ; +} diff --git a/app/login.tsx b/app/login.tsx index ffc1f70..1cdda6f 100644 --- a/app/login.tsx +++ b/app/login.tsx @@ -1,174 +1,212 @@ -import React, { useState } from 'react'; +import { useRouter } from "expo-router"; +import React, { useState } from "react"; import { - View, - Text, - TextInput, - TouchableOpacity, - StyleSheet, - Alert, - KeyboardAvoidingView, - Platform, - ScrollView, -} from 'react-native'; -import { useAuth } from '../src/context/AuthContext'; -import { useRouter } from 'expo-router'; + Alert, + KeyboardAvoidingView, + Platform, + ScrollView, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View, +} from "react-native"; +import { useColorScheme } from "../hooks/use-color-scheme"; +import { useAuth } from "../src/context/AuthContext"; export default function LoginScreen() { - const router = useRouter(); - const [identity, setIdentity] = useState(''); - const [password, setPassword] = useState(''); - const [isSuperuser, setIsSuperuser] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const { login } = useAuth(); + const router = useRouter(); + const colorScheme = useColorScheme() ?? "light"; + const isDark = colorScheme === "dark"; + const bgColor = isDark ? "#0b0b0d" : "#f5f5f5"; + const textColor = isDark ? "#fff" : "#333"; + const subText = isDark ? "#c6c6c8" : "#666"; + const inputBg = isDark ? "rgba(255,255,255,0.04)" : "#fff"; + const borderColor = isDark ? "rgba(255,255,255,0.06)" : "#ddd"; + const primary = isDark ? "#0A84FF" : "#007AFF"; - const handleLogin = async () => { - if (!identity || !password) { - Alert.alert('Error', 'Please enter both identity and password'); - return; - } + const { serverAddress: storedServerAddress, login } = useAuth(); + const [serverAddress, setServerAddress] = useState(storedServerAddress || ""); + const [identity, setIdentity] = useState(""); + const [password, setPassword] = useState(""); + const [isLoading, setIsLoading] = useState(false); - setIsLoading(true); - try { - await login(identity, password, isSuperuser); - router.replace('/'); - } catch (error: any) { - Alert.alert('Login Failed', error.message || 'An error occurred'); - } finally { - setIsLoading(false); - } - }; + const handleLogin = async () => { + if (!serverAddress || !identity || !password) { + Alert.alert("Error", "Please enter server address, identity, and password"); + return; + } - return ( - - - - UpSnap - Wake on LAN Mobile - + setIsLoading(true); + try { + await login(serverAddress, identity, password); + router.replace("/"); + } catch (error: any) { + Alert.alert("Login Failed", error.message || "An error occurred"); + } finally { + setIsLoading(false); + } + }; - - + return ( + + + + Remote WoL + + Mobile Frontend for UpSnap + + - + + - setIsSuperuser(!isSuperuser)} - activeOpacity={0.7} - > - - {isSuperuser && ✓} - - Login as Admin - + - - - {isLoading ? 'Logging in...' : 'Login'} - - - - - - ); + + + + + {isLoading ? "Logging in..." : "Login"} + + + + + + ); } const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - scrollContent: { - flexGrow: 1, - justifyContent: 'center', - padding: 20, - }, - header: { - alignItems: 'center', - marginBottom: 40, - }, - title: { - fontSize: 32, - fontWeight: 'bold', - color: '#333', - marginBottom: 8, - }, - subtitle: { - fontSize: 16, - color: '#666', - }, - form: { - width: '100%', - }, - input: { - backgroundColor: '#fff', - borderWidth: 1, - borderColor: '#ddd', - borderRadius: 8, - padding: 15, - fontSize: 16, - marginBottom: 15, - }, - checkboxContainer: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: 20, - }, - checkbox: { - width: 24, - height: 24, - borderWidth: 2, - borderColor: '#007AFF', - borderRadius: 4, - marginRight: 10, - justifyContent: 'center', - alignItems: 'center', - }, - checkboxChecked: { - backgroundColor: '#007AFF', - }, - checkmark: { - color: '#fff', - fontSize: 16, - fontWeight: 'bold', - }, - checkboxLabel: { - fontSize: 16, - color: '#333', - }, - button: { - backgroundColor: '#007AFF', - borderRadius: 8, - padding: 15, - alignItems: 'center', - }, - buttonDisabled: { - backgroundColor: '#ccc', - }, - buttonText: { - color: '#fff', - fontSize: 16, - fontWeight: 'bold', - }, + container: { + flex: 1, + backgroundColor: "#f5f5f5", + }, + scrollContent: { + flexGrow: 1, + justifyContent: "center", + padding: 20, + }, + header: { + alignItems: "center", + marginBottom: 40, + }, + title: { + fontSize: 32, + fontWeight: "bold", + color: "#333", + marginBottom: 8, + }, + subtitle: { + fontSize: 16, + color: "#666", + }, + form: { + width: "100%", + }, + input: { + backgroundColor: "#fff", + borderWidth: 1, + borderColor: "#ddd", + borderRadius: 8, + padding: 15, + fontSize: 16, + marginBottom: 15, + }, + checkboxContainer: { + flexDirection: "row", + alignItems: "center", + marginBottom: 20, + }, + checkbox: { + width: 24, + height: 24, + borderWidth: 2, + borderColor: "#007AFF", + borderRadius: 4, + marginRight: 10, + justifyContent: "center", + alignItems: "center", + }, + checkboxChecked: { + backgroundColor: "#007AFF", + }, + checkmark: { + color: "#fff", + fontSize: 16, + fontWeight: "bold", + }, + checkboxLabel: { + fontSize: 16, + color: "#333", + }, + button: { + backgroundColor: "#007AFF", + borderRadius: 8, + padding: 15, + alignItems: "center", + }, + buttonDisabled: { + backgroundColor: "#ccc", + }, + buttonText: { + color: "#fff", + fontSize: 16, + fontWeight: "bold", + }, }); diff --git a/app/scan-devices.tsx b/app/scan-devices.tsx index c0215e5..5f6a242 100644 --- a/app/scan-devices.tsx +++ b/app/scan-devices.tsx @@ -1,433 +1,356 @@ -import React, { useState } from 'react'; +import { useRouter } from "expo-router"; +import React, { useState } from "react"; import { - View, - Text, - FlatList, - TouchableOpacity, - StyleSheet, - Alert, - ActivityIndicator, - Modal, - TextInput, - ScrollView, -} from 'react-native'; -import { useRouter } from 'expo-router'; -import api from '../src/services/api'; -import { NetworkScanResult } from '../src/types'; + ActivityIndicator, + Alert, + FlatList, + StyleSheet, + Text, + TouchableOpacity, + View, +} from "react-native"; +import { useColorScheme } from "../hooks/use-color-scheme"; +import api from "../src/services/api"; +import { NetworkScanResult } from "../src/types"; +import { SymbolView } from "expo-symbols"; +import * as Burnt from "burnt"; export default function ScanDevicesScreen() { - const router = useRouter(); - const [scanning, setScanning] = useState(false); - const [devices, setDevices] = useState([]); - const [selectedDevice, setSelectedDevice] = useState(null); - const [showAddModal, setShowAddModal] = useState(false); - - const [formData, setFormData] = useState({ - name: '', - mac: '', - ip: '', - netmask: '255.255.255.0', - broadcast: '', - secureOnPassword: '', - port: '9', - }); + const router = useRouter(); + const colorScheme = useColorScheme() ?? "light"; + const isDark = colorScheme === "dark"; + const bgColor = isDark ? "#0b0b0d" : "#f5f5f5"; + const cardBg = isDark ? "rgba(255,255,255,0.04)" : "rgba(255, 255, 255, 0.8)"; + const primary = isDark ? "#0A84FF" : "#007AFF"; + const primaryPressed = isDark ? "#004BB5" : "#0051CC"; + const textColor = isDark ? "#fff" : "#333"; + const subText = isDark ? "#c6c6c8" : "#666"; - const handleScan = async () => { - setScanning(true); - try { - const results = await api.scanNetwork(); - setDevices(results); - } catch (error: any) { - Alert.alert('Error', error.message || 'Failed to scan network'); - } finally { - setScanning(false); - } - }; + const [scanning, setScanning] = useState(false); + const [devices, setDevices] = useState([]); - const handleAddFromScan = (device: NetworkScanResult) => { - const deviceName = device.name || device.hostname || `Device ${device.ip || device.ip_address}`; - const deviceIP = device.ip || device.ip_address || ''; - const deviceMAC = device.mac || device.mac_address || ''; - const deviceVendor = device.mac_vendor || ''; - - setFormData({ - name: deviceName, - mac: deviceMAC, - ip: deviceIP, - netmask: '255.255.255.0', - broadcast: '', - secureOnPassword: '', - port: '9', - }); - setSelectedDevice(device); - setShowAddModal(true); - }; + const handleScan = async () => { + setScanning(true); + try { + const results = await api.scanNetwork(); + setDevices(results); + } catch (error: any) { + Alert.alert("Error", error.message || "Failed to scan network"); + } finally { + setScanning(false); + } + }; - const handleSaveDevice = async () => { - if (!formData.name || !formData.mac || !formData.ip) { - Alert.alert('Error', 'Please fill in all required fields'); - return; - } + const handleAddFromScan = async (device: NetworkScanResult) => { + const deviceName = + device.name || + device.hostname || + `Device ${device.ip || device.ip_address}`; + const deviceIP = device.ip || device.ip_address || ""; + const deviceMAC = device.mac || device.mac_address || ""; - try { - await api.createDevice({ - name: formData.name, - mac: formData.mac, - ip: formData.ip, - netmask: formData.netmask, - broadcast: formData.broadcast, - secureOnPassword: formData.secureOnPassword, - port: parseInt(formData.port) || 9, - groups: [], - status: 'offline', - }); - Alert.alert('Success', 'Device added successfully'); - setShowAddModal(false); - router.back(); - } catch (error: any) { - Alert.alert('Error', error.message || 'Failed to add device'); - } - }; + try { + await api.createDevice({ + name: deviceName, + mac: deviceMAC, + ip: deviceIP, + netmask: "255.255.255.0", + broadcast: "", + secureOnPassword: "", + port: 9, + groups: [], + status: "offline", + }); + Burnt.toast({ + title: "Success", + preset: "done", + message: `Added ${deviceName} successfully`, + }); + router.back(); + } catch (error: any) { + Burnt.toast({ + title: "Error", + preset: "error", + message: error.message || "Failed to add device", + }); + } + }; - const renderDevice = ({ item }: { item: NetworkScanResult }) => { - const displayName = item.name || item.hostname || 'Unknown Device'; - const displayIP = item.ip || item.ip_address || ''; - const displayMAC = item.mac || item.mac_address || ''; - const displayVendor = item.mac_vendor || ''; - - return ( - handleAddFromScan(item)} - > - - {displayName} - {displayIP} - {displayMAC} - {displayVendor && displayVendor !== 'Unknown' && ( - {displayVendor} - )} - - - + - - - ); - }; + const renderDevice = ({ item }: { item: NetworkScanResult }) => { + const displayName = item.name || item.hostname || "Unknown Device"; + const displayIP = item.ip || item.ip_address || ""; + const displayMAC = item.mac || item.mac_address || ""; + const displayVendor = item.mac_vendor || ""; - return ( - - - Network Scan - - Discover devices on your local network - - + return ( + + + + {displayName} + + + {displayIP} + + + {displayMAC} + + {displayVendor && displayVendor !== "Unknown" && ( + + {displayVendor} + + )} + + handleAddFromScan(item)}> + + + + ); + }; - - {scanning ? ( - - ) : ( - Scan Network - )} - + return ( + + + + Add Devices + + + Discover devices on your local network + + - - Note: This requires the server to have nmap installed and may take several minutes. - + + {scanning ? ( + + ) : ( + + Scan Network + + )} + - {devices.length > 0 && ( - - Discovered Devices ({devices.length}) - `${item.ip || item.ip_address || index}-${index}`} - contentContainerStyle={styles.list} - /> - - )} + + Note: This requires the server to have nmap installed and may take + several minutes. + - {devices.length === 0 && !scanning && ( - - Tap "Scan Network" to discover devices - - )} + {devices.length > 0 && ( + + + Discovered Devices ({devices.length}) + + + `${item.ip || item.ip_address || index}-${index}` + } + contentContainerStyle={styles.list} + /> + + )} - setShowAddModal(false)} - > - - - - Add Device - setShowAddModal(false)}> - ✕ - - - - - - Device Name * - setFormData({ ...formData, name: text })} - placeholder="Device name" - /> - {selectedDevice?.mac_vendor && ( - - Vendor: {selectedDevice.mac_vendor} - - )} - - - - MAC Address * - setFormData({ ...formData, mac: text })} - placeholder="00:11:22:33:44:55" - autoCapitalize="characters" - /> - - - - IP Address * - setFormData({ ...formData, ip: text })} - placeholder="192.168.1.100" - keyboardType="numeric" - /> - - - - Netmask - setFormData({ ...formData, netmask: text })} - placeholder="255.255.255.0" - keyboardType="numeric" - /> - - - - Broadcast Address - setFormData({ ...formData, broadcast: text })} - placeholder="192.168.1.255" - keyboardType="numeric" - /> - - - - Port - setFormData({ ...formData, port: text })} - placeholder="9" - keyboardType="numeric" - /> - - - - Add Device - - - - - - - ); + {devices.length === 0 && !scanning && ( + + + Tap "Scan Network" to discover devices + + + )} + + ); } const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f5f5', - padding: 20, - }, - header: { - marginBottom: 20, - }, - headerText: { - fontSize: 24, - fontWeight: 'bold', - color: '#333', - marginBottom: 5, - }, - headerSubtext: { - fontSize: 14, - color: '#666', - }, - scanButton: { - backgroundColor: '#007AFF', - borderRadius: 12, - padding: 16, - alignItems: 'center', - marginBottom: 15, - }, - scanButtonDisabled: { - backgroundColor: '#ccc', - }, - scanButtonText: { - color: '#fff', - fontSize: 16, - fontWeight: 'bold', - }, - infoText: { - fontSize: 12, - color: '#999', - textAlign: 'center', - marginBottom: 20, - }, - resultsContainer: { - flex: 1, - }, - resultsHeader: { - fontSize: 18, - fontWeight: 'bold', - color: '#333', - marginBottom: 15, - }, - list: { - gap: 10, - }, - deviceCard: { - backgroundColor: 'rgba(255, 255, 255, 0.8)', - borderRadius: 16, - padding: 15, - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 8, - elevation: 3, - }, - deviceInfo: { - flex: 1, - }, - deviceName: { - fontSize: 16, - fontWeight: 'bold', - color: '#333', - marginBottom: 4, - }, - deviceDetail: { - fontSize: 14, - color: '#666', - marginBottom: 2, - }, - vendorText: { - fontSize: 12, - color: '#999', - fontStyle: 'italic', - }, - addButton: { - width: 44, - height: 44, - backgroundColor: 'rgba(76, 175, 80, 0.9)', - borderRadius: 22, - justifyContent: 'center', - alignItems: 'center', - }, - addButtonText: { - color: '#fff', - fontSize: 24, - fontWeight: 'bold', - }, - emptyContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - emptyText: { - fontSize: 16, - color: '#999', - textAlign: 'center', - }, - modalBlur: { - flex: 1, - justifyContent: 'flex-end', - }, - modalContent: { - backgroundColor: 'rgba(255, 255, 255, 0.95)', - borderTopLeftRadius: 24, - borderTopRightRadius: 24, - maxHeight: '90%', - }, - modalHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - padding: 20, - borderBottomWidth: 1, - borderBottomColor: 'rgba(0, 0, 0, 0.1)', - }, - modalTitle: { - fontSize: 20, - fontWeight: 'bold', - color: '#333', - }, - closeButton: { - fontSize: 24, - color: '#999', - padding: 8, - }, - modalBody: { - padding: 20, - }, - formGroup: { - marginBottom: 16, - }, - label: { - fontSize: 14, - fontWeight: '600', - color: '#333', - marginBottom: 8, - }, - input: { - backgroundColor: 'rgba(255, 255, 255, 0.8)', - borderWidth: 1, - borderColor: 'rgba(0, 0, 0, 0.1)', - borderRadius: 12, - padding: 14, - fontSize: 16, - }, - hint: { - fontSize: 12, - color: '#666', - marginTop: 4, - }, - saveButton: { - backgroundColor: '#4CAF50', - borderRadius: 12, - padding: 16, - alignItems: 'center', - marginTop: 10, - }, - saveButtonText: { - color: '#fff', - fontSize: 16, - fontWeight: 'bold', - }, + container: { + flex: 1, + backgroundColor: "#f5f5f5", + padding: 20, + }, + header: { + marginBottom: 20, + }, + headerText: { + fontSize: 24, + fontWeight: "bold", + color: "#333", + marginBottom: 5, + }, + headerSubtext: { + fontSize: 14, + color: "#666", + }, + scanButton: { + backgroundColor: "#007AFF", + borderRadius: 12, + padding: 16, + alignItems: "center", + marginBottom: 15, + }, + scanButtonText: { + color: "#fff", + fontSize: 16, + fontWeight: "bold", + }, + infoText: { + fontSize: 12, + color: "#999", + textAlign: "center", + marginBottom: 20, + }, + resultsContainer: { + flex: 1, + }, + resultsHeader: { + fontSize: 18, + fontWeight: "bold", + color: "#333", + marginBottom: 15, + }, + list: { + gap: 10, + }, + deviceCard: { + backgroundColor: "rgba(255, 255, 255, 0.8)", + borderRadius: 16, + padding: 15, + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 8, + elevation: 3, + }, + deviceInfo: { + flex: 1, + }, + deviceName: { + fontSize: 16, + fontWeight: "bold", + color: "#333", + marginBottom: 4, + }, + deviceDetail: { + fontSize: 14, + color: "#666", + marginBottom: 2, + }, + vendorText: { + fontSize: 12, + color: "#999", + fontStyle: "italic", + }, + addButton: { + width: 44, + height: 44, + borderRadius: 22, + justifyContent: "center", + alignItems: "center", + }, + addButtonText: { + color: "#fff", + fontSize: 24, + fontWeight: "bold", + }, + emptyContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + }, + emptyText: { + fontSize: 16, + color: "#999", + textAlign: "center", + }, + modalBlur: { + flex: 1, + justifyContent: "flex-end", + }, + modalContent: { + backgroundColor: "rgba(255, 255, 255, 0.95)", + borderTopLeftRadius: 24, + borderTopRightRadius: 24, + maxHeight: "90%", + }, + modalHeader: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + padding: 20, + borderBottomWidth: 1, + borderBottomColor: "rgba(0, 0, 0, 0.1)", + }, + modalTitle: { + fontSize: 20, + fontWeight: "bold", + color: "#333", + }, + closeButton: { + fontSize: 24, + color: "#999", + padding: 8, + }, + modalBody: { + padding: 20, + }, + formGroup: { + marginBottom: 16, + }, + label: { + fontSize: 14, + fontWeight: "600", + color: "#333", + marginBottom: 8, + }, + input: { + backgroundColor: "rgba(255, 255, 255, 0.8)", + borderWidth: 1, + borderColor: "rgba(0, 0, 0, 0.1)", + borderRadius: 12, + padding: 14, + fontSize: 16, + }, + hint: { + fontSize: 12, + color: "#666", + marginTop: 4, + }, + saveButton: { + backgroundColor: "#4CAF50", + borderRadius: 12, + padding: 16, + alignItems: "center", + marginTop: 10, + }, + saveButtonText: { + color: "#fff", + fontSize: 16, + fontWeight: "bold", + }, }); diff --git a/assets/images/splash-icon.png b/assets/images/splash-icon.png index 03d6f6b..ff83c47 100644 Binary files a/assets/images/splash-icon.png and b/assets/images/splash-icon.png differ diff --git a/assets/remotewol-ios.icon/Assets/gopher.svg b/assets/remotewol-ios.icon/Assets/gopher.svg new file mode 100644 index 0000000..ce565eb --- /dev/null +++ b/assets/remotewol-ios.icon/Assets/gopher.svg @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PACKAGE + + + + + + + + + + + + + + + + + + + + diff --git a/assets/remotewol-ios.icon/icon.json b/assets/remotewol-ios.icon/icon.json new file mode 100644 index 0000000..861340b --- /dev/null +++ b/assets/remotewol-ios.icon/icon.json @@ -0,0 +1,37 @@ +{ + "fill" : { + "automatic-gradient" : "extended-srgb:0.00000,0.53333,1.00000,1.00000" + }, + "groups" : [ + { + "layers" : [ + { + "glass" : true, + "image-name" : "gopher.svg", + "name" : "gopher", + "position" : { + "scale" : 0.45, + "translation-in-points" : [ + -6.017952794793246, + -0.4600234584925147 + ] + } + } + ], + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "translucency" : { + "enabled" : true, + "value" : 0.5 + } + } + ], + "supported-platforms" : { + "circles" : [ + "watchOS" + ], + "squares" : "shared" + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3b1db2c..6d66781 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@react-navigation/bottom-tabs": "^7.4.0", "@react-navigation/elements": "^2.6.3", "@react-navigation/native": "^7.1.8", + "burnt": "^0.13.0", "expo": "~54.0.30", "expo-constants": "~18.0.12", "expo-font": "~14.0.10", @@ -4677,6 +4678,40 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/burnt": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/burnt/-/burnt-0.13.0.tgz", + "integrity": "sha512-LjlQa7CLkGWUdz08YUIaGCJ8BLXib31/ztKqowgwqd7UH283A/kmdCj+1PYAQwDQEMPNmvSUfFHrjXbcwZibFQ==", + "license": "MIT", + "dependencies": { + "sf-symbols-typescript": "^1.0.0", + "sonner": "^2.0.1" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/burnt/node_modules/sf-symbols-typescript": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/sf-symbols-typescript/-/sf-symbols-typescript-1.0.0.tgz", + "integrity": "sha512-DkS7q3nN68dEMb4E18HFPDAvyrjDZK9YAQQF2QxeFu9gp2xRDXFMF8qLJ1EmQ/qeEGQmop4lmMM1WtYJTIcCMw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/burnt/node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", diff --git a/package.json b/package.json index cda4961..8175679 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@react-navigation/bottom-tabs": "^7.4.0", "@react-navigation/elements": "^2.6.3", "@react-navigation/native": "^7.1.8", + "burnt": "^0.13.0", "expo": "~54.0.30", "expo-constants": "~18.0.12", "expo-font": "~14.0.10", diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index 1f008d5..0f5ed5a 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -5,10 +5,11 @@ import { AuthResponse, User } from '../types'; interface AuthContextType { user: User | null; + serverAddress: string | null; token: string | null; isAuthenticated: boolean; isLoading: boolean; - login: (identity: string, password: string, isSuperuser?: boolean) => Promise; + login: (serverAddress: string, identity: string, password: string) => Promise; logout: () => Promise; } @@ -16,6 +17,7 @@ const AuthContext = createContext(undefined); export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [user, setUser] = useState(null); + const [serverAddress, setServerAddress] = useState(null); const [token, setToken] = useState(null); const [isLoading, setIsLoading] = useState(true); @@ -27,12 +29,18 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children try { const storedToken = await AsyncStorage.getItem('auth_token'); const storedUser = await AsyncStorage.getItem('auth_user'); + const storedServerAddress = await AsyncStorage.getItem('auth_server_address'); if (storedToken && storedUser) { setToken(storedToken); api.setToken(storedToken); setUser(JSON.parse(storedUser)); } + + if (storedServerAddress) { + setServerAddress(storedServerAddress); + api.setAddress(storedServerAddress + '/api'); + } } catch (error) { console.error('Failed to load auth', error); } finally { @@ -40,15 +48,17 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children } }; - const login = async (identity: string, password: string, isSuperuser = false) => { + const login = async (serverAddress: string, identity: string, password: string) => { try { - const response: AuthResponse = await api.authenticate(identity, password, isSuperuser); + const response: AuthResponse = await api.authenticate(serverAddress, identity, password); await AsyncStorage.setItem('auth_token', response.token); await AsyncStorage.setItem('auth_user', JSON.stringify(response.record)); + await AsyncStorage.setItem('auth_server_address', serverAddress); setToken(response.token); setUser(response.record); + setServerAddress(serverAddress); } catch (error) { throw error; } @@ -60,8 +70,10 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children await AsyncStorage.removeItem('auth_user'); api.clearToken(); + api.clearAddress(); setToken(null); setUser(null); + setServerAddress(null); } catch (error) { console.error('Failed to logout', error); } @@ -70,6 +82,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children return ( { - const endpoint = isSuperuser - ? `${API_BASE_URL}/collections/_superusers/auth-with-password` - : `${API_BASE_URL}/collections/users/auth-with-password`; - - const response = await fetch(endpoint, { - method: 'POST', - headers: this.getHeaders(), - body: JSON.stringify({ identity, password }), - }); + getAddress(): string | null { + return this.address; + } - if (!response.ok) { - const error = await response.json(); - throw new Error(error.message || 'Authentication failed'); - } + clearAddress() { + this.address = null; + } - const data: AuthResponse = await response.json(); - this.token = data.token; - return data; - } + private getHeaders(): HeadersInit { + const headers: HeadersInit = { + "Content-Type": "application/json", + }; - async getDevices(page = 1, perPage = 30): Promise { - const response = await fetch( - `${API_BASE_URL}/collections/devices/records?page=${page}&perPage=${perPage}`, - { - headers: this.getHeaders(), - } - ); + if (this.token) { + headers["Authorization"] = `Bearer ${this.token}`; + } - if (!response.ok) { - throw new Error('Failed to fetch devices'); - } + return headers; + } - const data = await response.json(); - return data.items; - } + async authenticate( + serverAddress: string, + identity: string, + password: string + ): Promise { + this.address = serverAddress + "/api"; - async getDevice(id: string): Promise { - const response = await fetch( - `${API_BASE_URL}/collections/devices/records/${id}`, - { - headers: this.getHeaders(), - } - ); + const response = await fetch( + `${this.address}/collections/users/auth-with-password`, + { + method: "POST", + headers: this.getHeaders(), + body: JSON.stringify({ identity, password }), + } + ); - if (!response.ok) { - throw new Error('Failed to fetch device'); - } + if (!response.ok) { + const response = await fetch( + `${this.address}/collections/_superusers/auth-with-password`, + { + method: "POST", + headers: this.getHeaders(), + body: JSON.stringify({ identity, password }), + } + ); - return response.json(); - } + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || "Authentication failed"); + } - async createDevice(device: Partial): Promise { - const response = await fetch( - `${API_BASE_URL}/collections/devices/records`, - { - method: 'POST', - headers: this.getHeaders(), - body: JSON.stringify(device), - } - ); + const data: AuthResponse = await response.json(); + this.token = data.token; + return data; + } - if (!response.ok) { - const error = await response.json(); - throw new Error(error.message || 'Failed to create device'); - } + const data: AuthResponse = await response.json(); + this.token = data.token; + return data; + } - return response.json(); - } + async getDevices(page = 1, perPage = 100): Promise { + const response = await fetch( + `${this.address}/collections/devices/records?page=${page}&perPage=${perPage}`, + { + headers: this.getHeaders(), + } + ); - async updateDevice(id: string, device: Partial): Promise { - const response = await fetch( - `${API_BASE_URL}/collections/devices/records/${id}`, - { - method: 'PATCH', - headers: this.getHeaders(), - body: JSON.stringify(device), - } - ); + if (!response.ok) { + throw new Error("Failed to fetch devices"); + } - if (!response.ok) { - const error = await response.json(); - throw new Error(error.message || 'Failed to update device'); - } + const data = await response.json(); + return data.items; + } - return response.json(); - } + async getDevice(id: string): Promise { + const response = await fetch( + `${this.address}/collections/devices/records/${id}`, + { + headers: this.getHeaders(), + } + ); - async deleteDevice(id: string): Promise { - const response = await fetch( - `${API_BASE_URL}/collections/devices/records/${id}`, - { - method: 'DELETE', - headers: this.getHeaders(), - } - ); + if (!response.ok) { + throw new Error("Failed to fetch device"); + } - if (!response.ok) { - throw new Error('Failed to delete device'); - } - } + return response.json(); + } - async wakeDevice(id: string): Promise { - const response = await fetch( - `${API_BASE_URL}/upsnap/wake/${id}`, - { - headers: this.getHeaders(), - } - ); + async createDevice(device: Partial): Promise { + const response = await fetch( + `${this.address}/collections/devices/records`, + { + method: "POST", + headers: this.getHeaders(), + body: JSON.stringify(device), + } + ); - if (!response.ok) { - throw new Error('Failed to wake device'); - } - } + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || "Failed to create device"); + } - async wakeGroup(id: string): Promise { - const response = await fetch( - `${API_BASE_URL}/upsnap/wakegroup/${id}`, - { - headers: this.getHeaders(), - } - ); + return response.json(); + } - if (!response.ok) { - throw new Error('Failed to wake group'); - } - } + async updateDevice(id: string, device: Partial): Promise { + const response = await fetch( + `${this.address}/collections/devices/records/${id}`, + { + method: "PATCH", + headers: this.getHeaders(), + body: JSON.stringify(device), + } + ); - async sleepDevice(id: string): Promise { - const response = await fetch( - `${API_BASE_URL}/upsnap/sleep/${id}`, - { - headers: this.getHeaders(), - } - ); + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || "Failed to update device"); + } - if (!response.ok) { - throw new Error('Failed to sleep device'); - } - } + return response.json(); + } - async rebootDevice(id: string): Promise { - const response = await fetch( - `${API_BASE_URL}/upsnap/reboot/${id}`, - { - headers: this.getHeaders(), - } - ); + async deleteDevice(id: string): Promise { + const response = await fetch( + `${this.address}/collections/devices/records/${id}`, + { + method: "DELETE", + headers: this.getHeaders(), + } + ); - if (!response.ok) { - throw new Error('Failed to reboot device'); - } - } + if (!response.ok) { + throw new Error("Failed to delete device"); + } + } - async shutdownDevice(id: string): Promise { - const response = await fetch( - `${API_BASE_URL}/upsnap/shutdown/${id}`, - { - headers: this.getHeaders(), - } - ); + async wakeDevice(id: string): Promise { + const response = await fetch(`${this.address}/upsnap/wake/${id}`, { + headers: this.getHeaders(), + }); - if (!response.ok) { - throw new Error('Failed to shutdown device'); - } - } + if (!response.ok) { + throw new Error("Failed to wake device"); + } + } - async scanNetwork(): Promise { - const response = await fetch( - `${API_BASE_URL}/upsnap/scan`, - { - headers: this.getHeaders(), - } - ); + async wakeGroup(id: string): Promise { + const response = await fetch(`${this.address}/upsnap/wakegroup/${id}`, { + headers: this.getHeaders(), + }); - if (!response.ok) { - throw new Error('Failed to scan network'); - } + if (!response.ok) { + throw new Error("Failed to wake group"); + } + } - const data = await response.json(); - console.log('Raw scan data:', data); - - if (data.devices && Array.isArray(data.devices)) { - return data.devices.map((item: any) => ({ - name: item.name || item.hostname || 'Unknown', - ip: item.ip || item.ip_address || '', - mac: item.mac || item.mac_address || '', - mac_vendor: item.mac_vendor || 'Unknown', - })); - } - - if (Array.isArray(data)) { - return data.map((item: any) => ({ - name: item.name || item.hostname || 'Unknown', - ip: item.ip || item.ip_address || '', - mac: item.mac || item.mac_address || '', - mac_vendor: item.mac_vendor || 'Unknown', - })); - } - - if (data.items && Array.isArray(data.items)) { - return data.items.map((item: any) => ({ - name: item.name || item.hostname || 'Unknown', - ip: item.ip || item.ip_address || '', - mac: item.mac || item.mac_address || '', - mac_vendor: item.mac_vendor || 'Unknown', - })); - } - - return []; - } + async sleepDevice(id: string): Promise { + const response = await fetch(`${this.address}/upsnap/sleep/${id}`, { + headers: this.getHeaders(), + }); + + if (!response.ok) { + throw new Error("Failed to sleep device"); + } + } + + async rebootDevice(id: string): Promise { + const response = await fetch(`${this.address}/upsnap/reboot/${id}`, { + headers: this.getHeaders(), + }); + + if (!response.ok) { + throw new Error("Failed to reboot device"); + } + } + + async shutdownDevice(id: string): Promise { + const response = await fetch(`${this.address}/upsnap/shutdown/${id}`, { + headers: this.getHeaders(), + }); + + if (!response.ok) { + throw new Error("Failed to shutdown device"); + } + } + + async scanNetwork(): Promise { + const response = await fetch(`${this.address}/upsnap/scan`, { + headers: this.getHeaders(), + }); + + if (!response.ok) { + throw new Error("Failed to scan network"); + } + + const data = await response.json(); + if (data.devices && Array.isArray(data.devices)) { + return data.devices.map((item: any) => ({ + name: item.name || item.hostname || "Unknown", + ip: item.ip || item.ip_address || "", + mac: item.mac || item.mac_address || "", + mac_vendor: item.mac_vendor || "Unknown", + })); + } + + if (Array.isArray(data)) { + return data.map((item: any) => ({ + name: item.name || item.hostname || "Unknown", + ip: item.ip || item.ip_address || "", + mac: item.mac || item.mac_address || "", + mac_vendor: item.mac_vendor || "Unknown", + })); + } + + if (data.items && Array.isArray(data.items)) { + return data.items.map((item: any) => ({ + name: item.name || item.hostname || "Unknown", + ip: item.ip || item.ip_address || "", + mac: item.mac || item.mac_address || "", + mac_vendor: item.mac_vendor || "Unknown", + })); + } + + return []; + } } export default new UpSnapAPI(); diff --git a/src/types/index.ts b/src/types/index.ts index 9877c34..a8951db 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,50 +1,50 @@ export interface Device { - id: string; - collectionId: string; - collectionName: string; - name: string; - mac: string; - ip: string; - netmask: string; - broadcast: string; - secureOnPassword: string; - port: number; - groups: string[]; - status: string; - created: string; - updated: string; + id: string; + collectionId: string; + collectionName: string; + name: string; + mac: string; + ip: string; + netmask: string; + broadcast: string; + secureOnPassword: string; + port: number; + groups: string[]; + status: string; + created: string; + updated: string; } export interface AuthResponse { - token: string; - record: User; + token: string; + record: User; } export interface User { - id: string; - collectionId: string; - collectionName: string; - username: string; - verified: boolean; - emailVisibility: boolean; - email: string; - created: string; - updated: string; - name: string; - avatar: number; + id: string; + collectionId: string; + collectionName: string; + username: string; + verified: boolean; + emailVisibility: boolean; + email: string; + created: string; + updated: string; + name: string; + avatar: number; } export interface DeviceGroup { - id: string; - name: string; + id: string; + name: string; } export interface NetworkScanResult { - name?: string; - hostname?: string; - ip?: string; - ip_address?: string; - mac?: string; - mac_address?: string; - mac_vendor?: string; + name?: string; + hostname?: string; + ip?: string; + ip_address?: string; + mac?: string; + mac_address?: string; + mac_vendor?: string; }