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