Matthias Nott
2026-03-07 af1543135d42adc2e97dc5243aeef7418cd3b00d
app/settings.tsx
....@@ -1,5 +1,6 @@
11 import React, { useCallback, useState } from "react";
22 import {
3
+ Alert,
34 Keyboard,
45 KeyboardAvoidingView,
56 Platform,
....@@ -13,32 +14,47 @@
1314 import { SafeAreaView } from "react-native-safe-area-context";
1415 import { router } from "expo-router";
1516 import { useConnection } from "../contexts/ConnectionContext";
17
+import { useTheme } from "../contexts/ThemeContext";
1618 import { StatusDot } from "../components/ui/StatusDot";
1719 import { ServerConfig } from "../types";
20
+import { sendWol, isValidMac } from "../services/wol";
21
+import { wsClient } from "../services/websocket";
1822
1923 export default function SettingsScreen() {
2024 const { serverConfig, status, connect, disconnect, saveServerConfig } =
2125 useConnection();
26
+ const { colors } = useTheme();
2227
23
- const [host, setHost] = useState(serverConfig?.host ?? "192.168.1.100");
28
+ const [host, setHost] = useState(serverConfig?.host ?? "");
29
+ const [localHost, setLocalHost] = useState(serverConfig?.localHost ?? "");
2430 const [port, setPort] = useState(
2531 serverConfig?.port ? String(serverConfig.port) : "8765"
2632 );
33
+ const [macAddress, setMacAddress] = useState(serverConfig?.macAddress ?? "");
2734 const [saved, setSaved] = useState(false);
35
+ const [waking, setWaking] = useState(false);
2836
2937 const handleSave = useCallback(async () => {
3038 const trimmedHost = host.trim();
3139 const portNum = parseInt(port.trim(), 10);
3240
33
- if (!trimmedHost || isNaN(portNum) || portNum < 1 || portNum > 65535) {
41
+ const trimmedLocal = localHost.trim();
42
+ if ((!trimmedHost && !trimmedLocal) || isNaN(portNum) || portNum < 1 || portNum > 65535) {
3443 return;
3544 }
3645
37
- const config: ServerConfig = { host: trimmedHost, port: portNum };
46
+ const trimmedMac = macAddress.trim();
47
+ const effectiveHost = trimmedHost || trimmedLocal;
48
+ const config: ServerConfig = {
49
+ host: effectiveHost,
50
+ port: portNum,
51
+ ...(trimmedLocal && trimmedHost ? { localHost: trimmedLocal } : {}),
52
+ ...(trimmedMac ? { macAddress: trimmedMac } : {}),
53
+ };
3854 await saveServerConfig(config);
3955 setSaved(true);
4056 setTimeout(() => setSaved(false), 2000);
41
- }, [host, port, saveServerConfig]);
57
+ }, [host, localHost, port, macAddress, saveServerConfig]);
4258
4359 const handleConnect = useCallback(() => {
4460 if (status === "connected" || status === "connecting") {
....@@ -48,17 +64,34 @@
4864 }
4965 }, [status, connect, disconnect]);
5066
51
- const isFormValid = host.trim().length > 0 && parseInt(port, 10) > 0;
67
+ const handleWake = useCallback(async () => {
68
+ const mac = macAddress.trim() || serverConfig?.macAddress;
69
+ if (!mac || !isValidMac(mac)) {
70
+ Alert.alert("Invalid MAC", "Enter a valid MAC address (e.g. 6a:8a:e7:b3:8e:5c)");
71
+ return;
72
+ }
73
+ setWaking(true);
74
+ try {
75
+ await sendWol(mac, host.trim() || serverConfig?.host);
76
+ Alert.alert("WoL Sent", "Magic packet sent. The Mac should wake in a few seconds.");
77
+ } catch (err) {
78
+ Alert.alert("WoL Failed", err instanceof Error ? err.message : String(err));
79
+ } finally {
80
+ setWaking(false);
81
+ }
82
+ }, [macAddress, host, serverConfig]);
83
+
84
+ const isFormValid = (host.trim().length > 0 || localHost.trim().length > 0) && parseInt(port, 10) > 0;
5285
5386 return (
54
- <SafeAreaView className="flex-1 bg-pai-bg" edges={["top", "bottom"]}>
87
+ <SafeAreaView style={{ flex: 1, backgroundColor: colors.bg }} edges={["top", "bottom"]}>
5588 <KeyboardAvoidingView
56
- className="flex-1"
89
+ style={{ flex: 1 }}
5790 behavior={Platform.OS === "ios" ? "padding" : "height"}
5891 >
5992 <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
6093 <ScrollView
61
- className="flex-1"
94
+ style={{ flex: 1 }}
6295 contentContainerStyle={{ paddingBottom: 32 }}
6396 keyboardShouldPersistTaps="handled"
6497 >
....@@ -70,7 +103,7 @@
70103 paddingHorizontal: 16,
71104 paddingVertical: 12,
72105 borderBottomWidth: 1,
73
- borderBottomColor: "#2E2E45",
106
+ borderBottomColor: colors.border,
74107 }}
75108 >
76109 <Pressable
....@@ -82,26 +115,42 @@
82115 alignItems: "center",
83116 justifyContent: "center",
84117 borderRadius: 18,
85
- backgroundColor: "#1E1E2E",
118
+ backgroundColor: colors.bgTertiary,
86119 marginRight: 12,
87120 }}
88121 >
89
- <Text style={{ color: "#E8E8F0", fontSize: 16 }}>←</Text>
122
+ <Text style={{ color: colors.text, fontSize: 16 }}>{"\u2190"}</Text>
90123 </Pressable>
91
- <Text style={{ color: "#E8E8F0", fontSize: 22, fontWeight: "800", letterSpacing: -0.5 }}>
124
+ <Text style={{ color: colors.text, fontSize: 22, fontWeight: "800", letterSpacing: -0.5 }}>
92125 Settings
93126 </Text>
94127 </View>
95128
96
- <View className="px-4 mt-6">
129
+ <View style={{ paddingHorizontal: 16, marginTop: 24 }}>
97130 {/* Connection status card */}
98
- <View className="bg-pai-surface rounded-2xl p-4 mb-6">
99
- <Text className="text-pai-text-secondary text-xs font-medium uppercase tracking-widest mb-3">
131
+ <View
132
+ style={{
133
+ backgroundColor: colors.bgTertiary,
134
+ borderRadius: 16,
135
+ padding: 16,
136
+ marginBottom: 24,
137
+ }}
138
+ >
139
+ <Text
140
+ style={{
141
+ color: colors.textSecondary,
142
+ fontSize: 11,
143
+ fontWeight: "500",
144
+ textTransform: "uppercase",
145
+ letterSpacing: 1.5,
146
+ marginBottom: 12,
147
+ }}
148
+ >
100149 Connection Status
101150 </Text>
102
- <View className="flex-row items-center gap-3">
151
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
103152 <StatusDot status={status} size={12} />
104
- <Text className="text-pai-text text-base font-medium">
153
+ <Text style={{ color: colors.text, fontSize: 16, fontWeight: "500" }}>
105154 {status === "connected"
106155 ? "Connected"
107156 : status === "connecting"
....@@ -110,47 +159,117 @@
110159 </Text>
111160 </View>
112161 {serverConfig && (
113
- <Text className="text-pai-text-muted text-sm mt-2">
114
- ws://{serverConfig.host}:{serverConfig.port}
162
+ <Text style={{ color: colors.textMuted, fontSize: 14, marginTop: 8 }}>
163
+ {wsClient.currentUrl || `ws://${serverConfig.host}:${serverConfig.port}`}
115164 </Text>
116165 )}
117166 </View>
118167
119168 {/* Server config */}
120
- <Text className="text-pai-text-secondary text-xs font-medium uppercase tracking-widest mb-3">
169
+ <Text
170
+ style={{
171
+ color: colors.textSecondary,
172
+ fontSize: 11,
173
+ fontWeight: "500",
174
+ textTransform: "uppercase",
175
+ letterSpacing: 1.5,
176
+ marginBottom: 12,
177
+ }}
178
+ >
121179 Server Configuration
122180 </Text>
123181
124
- <View className="bg-pai-surface rounded-2xl overflow-hidden mb-4">
125
- {/* Host */}
126
- <View className="px-4 py-3 border-b border-pai-border">
127
- <Text className="text-pai-text-muted text-xs mb-1">
128
- Host / IP Address
182
+ <View
183
+ style={{
184
+ backgroundColor: colors.bgTertiary,
185
+ borderRadius: 16,
186
+ overflow: "hidden",
187
+ marginBottom: 16,
188
+ }}
189
+ >
190
+ {/* Local Host (preferred when on same network) */}
191
+ <View
192
+ style={{
193
+ paddingHorizontal: 16,
194
+ paddingVertical: 12,
195
+ borderBottomWidth: 1,
196
+ borderBottomColor: colors.border,
197
+ }}
198
+ >
199
+ <Text style={{ color: colors.textMuted, fontSize: 11, marginBottom: 4 }}>
200
+ Local Address (optional)
201
+ </Text>
202
+ <TextInput
203
+ value={localHost}
204
+ onChangeText={setLocalHost}
205
+ placeholder="192.168.1.100"
206
+ placeholderTextColor={colors.textMuted}
207
+ autoCapitalize="none"
208
+ autoCorrect={false}
209
+ keyboardType="url"
210
+ style={{ color: colors.text, fontSize: 16, padding: 0 }}
211
+ />
212
+ </View>
213
+
214
+ {/* Remote Host (fallback / external) */}
215
+ <View
216
+ style={{
217
+ paddingHorizontal: 16,
218
+ paddingVertical: 12,
219
+ borderBottomWidth: 1,
220
+ borderBottomColor: colors.border,
221
+ }}
222
+ >
223
+ <Text style={{ color: colors.textMuted, fontSize: 11, marginBottom: 4 }}>
224
+ Remote Address
129225 </Text>
130226 <TextInput
131227 value={host}
132228 onChangeText={setHost}
133
- placeholder="192.168.1.100"
134
- placeholderTextColor="#5A5A78"
229
+ placeholder="myhost.example.com"
230
+ placeholderTextColor={colors.textMuted}
135231 autoCapitalize="none"
136232 autoCorrect={false}
137233 keyboardType="url"
138
- style={{ color: "#E8E8F0", fontSize: 16, padding: 0 }}
234
+ style={{ color: colors.text, fontSize: 16, padding: 0 }}
139235 />
140236 </View>
141237
142238 {/* Port */}
143
- <View className="px-4 py-3">
144
- <Text className="text-pai-text-muted text-xs mb-1">
239
+ <View
240
+ style={{
241
+ paddingHorizontal: 16,
242
+ paddingVertical: 12,
243
+ borderBottomWidth: 1,
244
+ borderBottomColor: colors.border,
245
+ }}
246
+ >
247
+ <Text style={{ color: colors.textMuted, fontSize: 11, marginBottom: 4 }}>
145248 Port
146249 </Text>
147250 <TextInput
148251 value={port}
149252 onChangeText={setPort}
150253 placeholder="8765"
151
- placeholderTextColor="#5A5A78"
254
+ placeholderTextColor={colors.textMuted}
152255 keyboardType="number-pad"
153
- style={{ color: "#E8E8F0", fontSize: 16, padding: 0 }}
256
+ style={{ color: colors.text, fontSize: 16, padding: 0 }}
257
+ />
258
+ </View>
259
+
260
+ {/* MAC Address for Wake-on-LAN */}
261
+ <View style={{ paddingHorizontal: 16, paddingVertical: 12 }}>
262
+ <Text style={{ color: colors.textMuted, fontSize: 11, marginBottom: 4 }}>
263
+ MAC Address (Wake-on-LAN)
264
+ </Text>
265
+ <TextInput
266
+ value={macAddress}
267
+ onChangeText={setMacAddress}
268
+ placeholder="6a:8a:e7:b3:8e:5c"
269
+ placeholderTextColor={colors.textMuted}
270
+ autoCapitalize="none"
271
+ autoCorrect={false}
272
+ style={{ color: colors.text, fontSize: 16, padding: 0 }}
154273 />
155274 </View>
156275 </View>
....@@ -159,16 +278,51 @@
159278 <Pressable
160279 onPress={handleSave}
161280 disabled={!isFormValid}
162
- className={`rounded-2xl py-4 items-center mb-3 ${
163
- isFormValid ? "bg-pai-accent" : "bg-pai-surface"
164
- }`}
281
+ style={{
282
+ borderRadius: 16,
283
+ paddingVertical: 16,
284
+ alignItems: "center",
285
+ marginBottom: 12,
286
+ backgroundColor: isFormValid ? colors.accent : colors.bgTertiary,
287
+ }}
165288 >
166289 <Text
167
- className={`text-base font-semibold ${
168
- isFormValid ? "text-white" : "text-pai-text-muted"
169
- }`}
290
+ style={{
291
+ fontSize: 16,
292
+ fontWeight: "600",
293
+ color: isFormValid ? "#FFF" : colors.textMuted,
294
+ }}
170295 >
171296 {saved ? "Saved!" : "Save Configuration"}
297
+ </Text>
298
+ </Pressable>
299
+
300
+ {/* Wake-on-LAN button */}
301
+ <Pressable
302
+ onPress={handleWake}
303
+ disabled={waking || (!macAddress.trim() && !serverConfig?.macAddress)}
304
+ style={{
305
+ borderRadius: 16,
306
+ paddingVertical: 16,
307
+ alignItems: "center",
308
+ marginBottom: 12,
309
+ backgroundColor:
310
+ macAddress.trim() || serverConfig?.macAddress
311
+ ? "#FF950033"
312
+ : colors.bgTertiary,
313
+ }}
314
+ >
315
+ <Text
316
+ style={{
317
+ fontSize: 16,
318
+ fontWeight: "600",
319
+ color:
320
+ macAddress.trim() || serverConfig?.macAddress
321
+ ? "#FF9500"
322
+ : colors.textMuted,
323
+ }}
324
+ >
325
+ {waking ? "Sending..." : "Wake Mac (WoL)"}
172326 </Text>
173327 </Pressable>
174328
....@@ -176,18 +330,22 @@
176330 <Pressable
177331 onPress={handleConnect}
178332 disabled={!serverConfig}
179
- className={`rounded-2xl py-4 items-center ${
180
- status === "connected"
181
- ? "bg-pai-error/20"
182
- : "bg-pai-success/20"
183
- }`}
333
+ style={{
334
+ borderRadius: 16,
335
+ paddingVertical: 16,
336
+ alignItems: "center",
337
+ backgroundColor:
338
+ status === "connected"
339
+ ? colors.danger + "33"
340
+ : "#2ED57333",
341
+ }}
184342 >
185343 <Text
186
- className={`text-base font-semibold ${
187
- status === "connected"
188
- ? "text-pai-error"
189
- : "text-pai-success"
190
- }`}
344
+ style={{
345
+ fontSize: 16,
346
+ fontWeight: "600",
347
+ color: status === "connected" ? colors.danger : "#2ED573",
348
+ }}
191349 >
192350 {status === "connected"
193351 ? "Disconnect"