From af1543135d42adc2e97dc5243aeef7418cd3b00d Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Sat, 07 Mar 2026 08:39:26 +0100
Subject: [PATCH] feat: dual address auto-switch, custom icon, notifications, image support
---
app/settings.tsx | 254 +++++++++++++++++++++++++++++++++++++++++---------
1 files changed, 206 insertions(+), 48 deletions(-)
diff --git a/app/settings.tsx b/app/settings.tsx
index 3b36869..c4da07e 100644
--- a/app/settings.tsx
+++ b/app/settings.tsx
@@ -1,5 +1,6 @@
import React, { useCallback, useState } from "react";
import {
+ Alert,
Keyboard,
KeyboardAvoidingView,
Platform,
@@ -13,32 +14,47 @@
import { SafeAreaView } from "react-native-safe-area-context";
import { router } from "expo-router";
import { useConnection } from "../contexts/ConnectionContext";
+import { useTheme } from "../contexts/ThemeContext";
import { StatusDot } from "../components/ui/StatusDot";
import { ServerConfig } from "../types";
+import { sendWol, isValidMac } from "../services/wol";
+import { wsClient } from "../services/websocket";
export default function SettingsScreen() {
const { serverConfig, status, connect, disconnect, saveServerConfig } =
useConnection();
+ const { colors } = useTheme();
- const [host, setHost] = useState(serverConfig?.host ?? "192.168.1.100");
+ const [host, setHost] = useState(serverConfig?.host ?? "");
+ const [localHost, setLocalHost] = useState(serverConfig?.localHost ?? "");
const [port, setPort] = useState(
serverConfig?.port ? String(serverConfig.port) : "8765"
);
+ const [macAddress, setMacAddress] = useState(serverConfig?.macAddress ?? "");
const [saved, setSaved] = useState(false);
+ const [waking, setWaking] = useState(false);
const handleSave = useCallback(async () => {
const trimmedHost = host.trim();
const portNum = parseInt(port.trim(), 10);
- if (!trimmedHost || isNaN(portNum) || portNum < 1 || portNum > 65535) {
+ const trimmedLocal = localHost.trim();
+ if ((!trimmedHost && !trimmedLocal) || isNaN(portNum) || portNum < 1 || portNum > 65535) {
return;
}
- const config: ServerConfig = { host: trimmedHost, port: portNum };
+ const trimmedMac = macAddress.trim();
+ const effectiveHost = trimmedHost || trimmedLocal;
+ const config: ServerConfig = {
+ host: effectiveHost,
+ port: portNum,
+ ...(trimmedLocal && trimmedHost ? { localHost: trimmedLocal } : {}),
+ ...(trimmedMac ? { macAddress: trimmedMac } : {}),
+ };
await saveServerConfig(config);
setSaved(true);
setTimeout(() => setSaved(false), 2000);
- }, [host, port, saveServerConfig]);
+ }, [host, localHost, port, macAddress, saveServerConfig]);
const handleConnect = useCallback(() => {
if (status === "connected" || status === "connecting") {
@@ -48,17 +64,34 @@
}
}, [status, connect, disconnect]);
- const isFormValid = host.trim().length > 0 && parseInt(port, 10) > 0;
+ const handleWake = useCallback(async () => {
+ const mac = macAddress.trim() || serverConfig?.macAddress;
+ if (!mac || !isValidMac(mac)) {
+ Alert.alert("Invalid MAC", "Enter a valid MAC address (e.g. 6a:8a:e7:b3:8e:5c)");
+ return;
+ }
+ setWaking(true);
+ try {
+ await sendWol(mac, host.trim() || serverConfig?.host);
+ Alert.alert("WoL Sent", "Magic packet sent. The Mac should wake in a few seconds.");
+ } catch (err) {
+ Alert.alert("WoL Failed", err instanceof Error ? err.message : String(err));
+ } finally {
+ setWaking(false);
+ }
+ }, [macAddress, host, serverConfig]);
+
+ const isFormValid = (host.trim().length > 0 || localHost.trim().length > 0) && parseInt(port, 10) > 0;
return (
- <SafeAreaView className="flex-1 bg-pai-bg" edges={["top", "bottom"]}>
+ <SafeAreaView style={{ flex: 1, backgroundColor: colors.bg }} edges={["top", "bottom"]}>
<KeyboardAvoidingView
- className="flex-1"
+ style={{ flex: 1 }}
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<ScrollView
- className="flex-1"
+ style={{ flex: 1 }}
contentContainerStyle={{ paddingBottom: 32 }}
keyboardShouldPersistTaps="handled"
>
@@ -70,7 +103,7 @@
paddingHorizontal: 16,
paddingVertical: 12,
borderBottomWidth: 1,
- borderBottomColor: "#2E2E45",
+ borderBottomColor: colors.border,
}}
>
<Pressable
@@ -82,26 +115,42 @@
alignItems: "center",
justifyContent: "center",
borderRadius: 18,
- backgroundColor: "#1E1E2E",
+ backgroundColor: colors.bgTertiary,
marginRight: 12,
}}
>
- <Text style={{ color: "#E8E8F0", fontSize: 16 }}>←</Text>
+ <Text style={{ color: colors.text, fontSize: 16 }}>{"\u2190"}</Text>
</Pressable>
- <Text style={{ color: "#E8E8F0", fontSize: 22, fontWeight: "800", letterSpacing: -0.5 }}>
+ <Text style={{ color: colors.text, fontSize: 22, fontWeight: "800", letterSpacing: -0.5 }}>
Settings
</Text>
</View>
- <View className="px-4 mt-6">
+ <View style={{ paddingHorizontal: 16, marginTop: 24 }}>
{/* Connection status card */}
- <View className="bg-pai-surface rounded-2xl p-4 mb-6">
- <Text className="text-pai-text-secondary text-xs font-medium uppercase tracking-widest mb-3">
+ <View
+ style={{
+ backgroundColor: colors.bgTertiary,
+ borderRadius: 16,
+ padding: 16,
+ marginBottom: 24,
+ }}
+ >
+ <Text
+ style={{
+ color: colors.textSecondary,
+ fontSize: 11,
+ fontWeight: "500",
+ textTransform: "uppercase",
+ letterSpacing: 1.5,
+ marginBottom: 12,
+ }}
+ >
Connection Status
</Text>
- <View className="flex-row items-center gap-3">
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
<StatusDot status={status} size={12} />
- <Text className="text-pai-text text-base font-medium">
+ <Text style={{ color: colors.text, fontSize: 16, fontWeight: "500" }}>
{status === "connected"
? "Connected"
: status === "connecting"
@@ -110,47 +159,117 @@
</Text>
</View>
{serverConfig && (
- <Text className="text-pai-text-muted text-sm mt-2">
- ws://{serverConfig.host}:{serverConfig.port}
+ <Text style={{ color: colors.textMuted, fontSize: 14, marginTop: 8 }}>
+ {wsClient.currentUrl || `ws://${serverConfig.host}:${serverConfig.port}`}
</Text>
)}
</View>
{/* Server config */}
- <Text className="text-pai-text-secondary text-xs font-medium uppercase tracking-widest mb-3">
+ <Text
+ style={{
+ color: colors.textSecondary,
+ fontSize: 11,
+ fontWeight: "500",
+ textTransform: "uppercase",
+ letterSpacing: 1.5,
+ marginBottom: 12,
+ }}
+ >
Server Configuration
</Text>
- <View className="bg-pai-surface rounded-2xl overflow-hidden mb-4">
- {/* Host */}
- <View className="px-4 py-3 border-b border-pai-border">
- <Text className="text-pai-text-muted text-xs mb-1">
- Host / IP Address
+ <View
+ style={{
+ backgroundColor: colors.bgTertiary,
+ borderRadius: 16,
+ overflow: "hidden",
+ marginBottom: 16,
+ }}
+ >
+ {/* Local Host (preferred when on same network) */}
+ <View
+ style={{
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ borderBottomWidth: 1,
+ borderBottomColor: colors.border,
+ }}
+ >
+ <Text style={{ color: colors.textMuted, fontSize: 11, marginBottom: 4 }}>
+ Local Address (optional)
+ </Text>
+ <TextInput
+ value={localHost}
+ onChangeText={setLocalHost}
+ placeholder="192.168.1.100"
+ placeholderTextColor={colors.textMuted}
+ autoCapitalize="none"
+ autoCorrect={false}
+ keyboardType="url"
+ style={{ color: colors.text, fontSize: 16, padding: 0 }}
+ />
+ </View>
+
+ {/* Remote Host (fallback / external) */}
+ <View
+ style={{
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ borderBottomWidth: 1,
+ borderBottomColor: colors.border,
+ }}
+ >
+ <Text style={{ color: colors.textMuted, fontSize: 11, marginBottom: 4 }}>
+ Remote Address
</Text>
<TextInput
value={host}
onChangeText={setHost}
- placeholder="192.168.1.100"
- placeholderTextColor="#5A5A78"
+ placeholder="myhost.example.com"
+ placeholderTextColor={colors.textMuted}
autoCapitalize="none"
autoCorrect={false}
keyboardType="url"
- style={{ color: "#E8E8F0", fontSize: 16, padding: 0 }}
+ style={{ color: colors.text, fontSize: 16, padding: 0 }}
/>
</View>
{/* Port */}
- <View className="px-4 py-3">
- <Text className="text-pai-text-muted text-xs mb-1">
+ <View
+ style={{
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ borderBottomWidth: 1,
+ borderBottomColor: colors.border,
+ }}
+ >
+ <Text style={{ color: colors.textMuted, fontSize: 11, marginBottom: 4 }}>
Port
</Text>
<TextInput
value={port}
onChangeText={setPort}
placeholder="8765"
- placeholderTextColor="#5A5A78"
+ placeholderTextColor={colors.textMuted}
keyboardType="number-pad"
- style={{ color: "#E8E8F0", fontSize: 16, padding: 0 }}
+ style={{ color: colors.text, fontSize: 16, padding: 0 }}
+ />
+ </View>
+
+ {/* MAC Address for Wake-on-LAN */}
+ <View style={{ paddingHorizontal: 16, paddingVertical: 12 }}>
+ <Text style={{ color: colors.textMuted, fontSize: 11, marginBottom: 4 }}>
+ MAC Address (Wake-on-LAN)
+ </Text>
+ <TextInput
+ value={macAddress}
+ onChangeText={setMacAddress}
+ placeholder="6a:8a:e7:b3:8e:5c"
+ placeholderTextColor={colors.textMuted}
+ autoCapitalize="none"
+ autoCorrect={false}
+ style={{ color: colors.text, fontSize: 16, padding: 0 }}
/>
</View>
</View>
@@ -159,16 +278,51 @@
<Pressable
onPress={handleSave}
disabled={!isFormValid}
- className={`rounded-2xl py-4 items-center mb-3 ${
- isFormValid ? "bg-pai-accent" : "bg-pai-surface"
- }`}
+ style={{
+ borderRadius: 16,
+ paddingVertical: 16,
+ alignItems: "center",
+ marginBottom: 12,
+ backgroundColor: isFormValid ? colors.accent : colors.bgTertiary,
+ }}
>
<Text
- className={`text-base font-semibold ${
- isFormValid ? "text-white" : "text-pai-text-muted"
- }`}
+ style={{
+ fontSize: 16,
+ fontWeight: "600",
+ color: isFormValid ? "#FFF" : colors.textMuted,
+ }}
>
{saved ? "Saved!" : "Save Configuration"}
+ </Text>
+ </Pressable>
+
+ {/* Wake-on-LAN button */}
+ <Pressable
+ onPress={handleWake}
+ disabled={waking || (!macAddress.trim() && !serverConfig?.macAddress)}
+ style={{
+ borderRadius: 16,
+ paddingVertical: 16,
+ alignItems: "center",
+ marginBottom: 12,
+ backgroundColor:
+ macAddress.trim() || serverConfig?.macAddress
+ ? "#FF950033"
+ : colors.bgTertiary,
+ }}
+ >
+ <Text
+ style={{
+ fontSize: 16,
+ fontWeight: "600",
+ color:
+ macAddress.trim() || serverConfig?.macAddress
+ ? "#FF9500"
+ : colors.textMuted,
+ }}
+ >
+ {waking ? "Sending..." : "Wake Mac (WoL)"}
</Text>
</Pressable>
@@ -176,18 +330,22 @@
<Pressable
onPress={handleConnect}
disabled={!serverConfig}
- className={`rounded-2xl py-4 items-center ${
- status === "connected"
- ? "bg-pai-error/20"
- : "bg-pai-success/20"
- }`}
+ style={{
+ borderRadius: 16,
+ paddingVertical: 16,
+ alignItems: "center",
+ backgroundColor:
+ status === "connected"
+ ? colors.danger + "33"
+ : "#2ED57333",
+ }}
>
<Text
- className={`text-base font-semibold ${
- status === "connected"
- ? "text-pai-error"
- : "text-pai-success"
- }`}
+ style={{
+ fontSize: 16,
+ fontWeight: "600",
+ color: status === "connected" ? colors.danger : "#2ED573",
+ }}
>
{status === "connected"
? "Disconnect"
--
Gitblit v1.3.1