diff options
Diffstat (limited to 'apps')
| -rw-r--r-- | apps/mobile/app/_layout.tsx | 8 | ||||
| -rw-r--r-- | apps/mobile/app/server-address.tsx | 230 | ||||
| -rw-r--r-- | apps/mobile/app/signin.tsx | 103 |
3 files changed, 257 insertions, 84 deletions
diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx index 1e6128c7..3f9e5575 100644 --- a/apps/mobile/app/_layout.tsx +++ b/apps/mobile/app/_layout.tsx @@ -64,6 +64,14 @@ export default function RootLayout() { /> <Stack.Screen name="sharing" /> <Stack.Screen + name="server-address" + options={{ + title: "Server Address", + headerShown: true, + presentation: "modal", + }} + /> + <Stack.Screen name="test-connection" options={{ title: "Test Connection", diff --git a/apps/mobile/app/server-address.tsx b/apps/mobile/app/server-address.tsx new file mode 100644 index 00000000..b63aaf4c --- /dev/null +++ b/apps/mobile/app/server-address.tsx @@ -0,0 +1,230 @@ +import { useState } from "react"; +import { Pressable, View } from "react-native"; +import { KeyboardAwareScrollView } from "react-native-keyboard-controller"; +import { Stack, useRouter } from "expo-router"; +import { Button } from "@/components/ui/Button"; +import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; +import { Input } from "@/components/ui/Input"; +import PageTitle from "@/components/ui/PageTitle"; +import { Text } from "@/components/ui/Text"; +import useAppSettings from "@/lib/settings"; +import { Plus, Trash2 } from "lucide-react-native"; +import { useColorScheme } from "nativewind"; + +export default function ServerAddress() { + const router = useRouter(); + const { colorScheme } = useColorScheme(); + const iconColor = colorScheme === "dark" ? "#d1d5db" : "#374151"; + const { settings, setSettings } = useAppSettings(); + const [address, setAddress] = useState( + settings.address ?? "https://cloud.karakeep.app", + ); + const [error, setError] = useState<string | undefined>(); + + // Custom headers state + const [headers, setHeaders] = useState<{ key: string; value: string }[]>( + Object.entries(settings.customHeaders || {}).map(([key, value]) => ({ + key, + value, + })), + ); + const [newHeaderKey, setNewHeaderKey] = useState(""); + const [newHeaderValue, setNewHeaderValue] = useState(""); + + const handleAddHeader = () => { + if (!newHeaderKey.trim() || !newHeaderValue.trim()) { + return; + } + + // Check if header already exists + const existingIndex = headers.findIndex((h) => h.key === newHeaderKey); + if (existingIndex >= 0) { + // Update existing header + const updatedHeaders = [...headers]; + updatedHeaders[existingIndex].value = newHeaderValue; + setHeaders(updatedHeaders); + } else { + // Add new header + setHeaders([...headers, { key: newHeaderKey, value: newHeaderValue }]); + } + + setNewHeaderKey(""); + setNewHeaderValue(""); + }; + + const handleRemoveHeader = (index: number) => { + setHeaders(headers.filter((_, i) => i !== index)); + }; + + const handleSave = () => { + // Validate the address + if (!address.trim()) { + setError("Server address is required"); + return; + } + + if (!address.startsWith("http://") && !address.startsWith("https://")) { + setError("Server address must start with http:// or https://"); + return; + } + + // Convert headers array to object + const headersObject = headers.reduce( + (acc, { key, value }) => { + if (key.trim() && value.trim()) { + acc[key] = value; + } + return acc; + }, + {} as Record<string, string>, + ); + + // Remove trailing slash and save + const cleanedAddress = address.trim().replace(/\/$/, ""); + setSettings({ + ...settings, + address: cleanedAddress, + customHeaders: headersObject, + }); + router.back(); + }; + + return ( + <CustomSafeAreaView> + <Stack.Screen + options={{ + title: "Server Address", + headerRight: () => ( + <Pressable onPress={handleSave}> + <Text className="text-base font-semibold text-blue-500"> + Save + </Text> + </Pressable> + ), + }} + /> + <PageTitle title="Server Address" /> + <KeyboardAwareScrollView + className="w-full" + contentContainerClassName="items-center gap-4 px-4 py-4" + bottomOffset={20} + keyboardShouldPersistTaps="handled" + > + {/* Error Message */} + {error && ( + <View className="w-full rounded-lg bg-red-50 p-3 dark:bg-red-950"> + <Text className="text-center text-sm text-red-600 dark:text-red-400"> + {error} + </Text> + </View> + )} + + {/* Server Address Section */} + <View className="w-full"> + <Text className="mb-2 px-1 text-sm font-medium text-muted-foreground"> + Server URL + </Text> + <View className="w-full gap-3 rounded-lg bg-card px-4 py-4"> + <Text className="text-sm text-muted-foreground"> + Enter the URL of your Karakeep server + </Text> + <Input + placeholder="https://cloud.karakeep.app" + value={address} + onChangeText={(text) => { + setAddress(text); + setError(undefined); + }} + autoCapitalize="none" + keyboardType="url" + autoFocus + inputClasses="bg-background" + /> + <Text className="text-xs text-muted-foreground"> + Must start with http:// or https:// + </Text> + </View> + </View> + + {/* Custom Headers Section */} + <View className="w-full"> + <Text className="mb-2 px-1 text-sm font-medium text-muted-foreground"> + Custom Headers + {headers.length > 0 && ( + <Text className="text-muted-foreground"> ({headers.length})</Text> + )} + </Text> + <View className="w-full gap-3 rounded-lg bg-card px-4 py-4"> + <Text className="text-sm text-muted-foreground"> + Add custom HTTP headers for API requests + </Text> + + {/* Existing Headers List */} + {headers.length === 0 ? ( + <View className="py-4"> + <Text className="text-center text-sm text-muted-foreground"> + No custom headers configured + </Text> + </View> + ) : ( + <View className="gap-2"> + {headers.map((header, index) => ( + <View + key={index} + className="flex-row items-center gap-3 rounded-lg border border-border bg-background p-3" + > + <View className="flex-1 gap-1"> + <Text className="text-sm font-semibold"> + {header.key} + </Text> + <Text + className="text-xs text-muted-foreground" + numberOfLines={1} + > + {header.value} + </Text> + </View> + <Pressable + onPress={() => handleRemoveHeader(index)} + className="rounded-md p-2" + hitSlop={8} + > + <Trash2 size={18} color="#ef4444" /> + </Pressable> + </View> + ))} + </View> + )} + + {/* Add New Header Form */} + <View className="gap-2 border-t border-border pt-4"> + <Text className="text-sm font-medium">Add New Header</Text> + <Input + placeholder="Header Name (e.g., X-Custom-Header)" + value={newHeaderKey} + onChangeText={setNewHeaderKey} + autoCapitalize="none" + inputClasses="bg-background" + /> + <Input + placeholder="Header Value" + value={newHeaderValue} + onChangeText={setNewHeaderValue} + autoCapitalize="none" + inputClasses="bg-background" + /> + <Button + variant="secondary" + onPress={handleAddHeader} + disabled={!newHeaderKey.trim() || !newHeaderValue.trim()} + > + <Plus size={16} color={iconColor} /> + <Text className="text-sm">Add Header</Text> + </Button> + </View> + </View> + </View> + </KeyboardAwareScrollView> + </CustomSafeAreaView> + ); +} diff --git a/apps/mobile/app/signin.tsx b/apps/mobile/app/signin.tsx index 6a554f89..03cbba5a 100644 --- a/apps/mobile/app/signin.tsx +++ b/apps/mobile/app/signin.tsx @@ -7,7 +7,6 @@ import { View, } from "react-native"; import { Redirect, useRouter } from "expo-router"; -import { CustomHeadersModal } from "@/components/CustomHeadersModal"; import Logo from "@/components/Logo"; import { TailwindResolver } from "@/components/TailwindResolver"; import { Button } from "@/components/ui/Button"; @@ -15,7 +14,7 @@ import { Input } from "@/components/ui/Input"; import { Text } from "@/components/ui/Text"; import useAppSettings from "@/lib/settings"; import { api } from "@/lib/trpc"; -import { Bug, Check, Edit3 } from "lucide-react-native"; +import { Bug, Edit3 } from "lucide-react-native"; enum LoginType { Password, @@ -28,12 +27,6 @@ export default function Signin() { const [error, setError] = useState<string | undefined>(); const [loginType, setLoginType] = useState<LoginType>(LoginType.Password); - const [isEditingServerAddress, setIsEditingServerAddress] = useState(false); - const [tempServerAddress, setTempServerAddress] = useState( - settings.address ?? "https://cloud.karakeep.app", - ); - const [isCustomHeadersModalVisible, setIsCustomHeadersModalVisible] = - useState(false); const emailRef = useRef<string>(""); const passwordRef = useRef<string>(""); @@ -82,19 +75,15 @@ export default function Signin() { return <Redirect href="dashboard" />; } - const handleSaveCustomHeaders = (headers: Record<string, string>) => { - setSettings({ ...settings, customHeaders: headers }); - }; - const onSignin = () => { - if (!tempServerAddress) { + if (!settings.address) { setError("Server address is required"); return; } if ( - !tempServerAddress.startsWith("http://") && - !tempServerAddress.startsWith("https://") + !settings.address.startsWith("http://") && + !settings.address.startsWith("https://") ) { setError("Server address must start with http:// or https://"); return; @@ -137,71 +126,23 @@ export default function Signin() { )} <View className="gap-2"> <Text className="font-bold">Server Address</Text> - {!isEditingServerAddress ? ( - <View className="flex-row items-center gap-2"> - <View className="flex-1 rounded-md border border-border bg-card px-3 py-2"> - <Text>{tempServerAddress}</Text> - </View> - <Button - size="icon" - variant="secondary" - onPress={() => { - setIsEditingServerAddress(true); - }} - > - <TailwindResolver - comp={(styles) => ( - <Edit3 size={16} color={styles?.color?.toString()} /> - )} - className="color-foreground" - /> - </Button> + <View className="flex-row items-center gap-2"> + <View className="flex-1 rounded-md border border-border bg-card px-3 py-2"> + <Text>{settings.address ?? "https://cloud.karakeep.app"}</Text> </View> - ) : ( - <View className="flex-row items-center gap-2"> - <Input - className="flex-1" - inputClasses="bg-card" - placeholder="Server Address" - value={tempServerAddress} - autoCapitalize="none" - keyboardType="url" - onChangeText={setTempServerAddress} - autoFocus + <Button + size="icon" + variant="secondary" + onPress={() => router.push("/server-address")} + > + <TailwindResolver + comp={(styles) => ( + <Edit3 size={16} color={styles?.color?.toString()} /> + )} + className="color-foreground" /> - <Button - size="icon" - variant="primary" - onPress={() => { - if (tempServerAddress.trim()) { - setSettings({ - ...settings, - address: tempServerAddress.trim().replace(/\/$/, ""), - }); - } - setIsEditingServerAddress(false); - }} - > - <TailwindResolver - comp={(styles) => ( - <Check size={16} color={styles?.color?.toString()} /> - )} - className="text-white" - /> - </Button> - </View> - )} - <Pressable - onPress={() => setIsCustomHeadersModalVisible(true)} - className="mt-1" - > - <Text className="text-xs text-gray-500 underline"> - Configure Custom Headers{" "} - {settings.customHeaders && - Object.keys(settings.customHeaders).length > 0 && - `(${Object.keys(settings.customHeaders).length})`} - </Text> - </Pressable> + </Button> + </View> </View> {loginType === LoginType.Password && ( <> @@ -282,12 +223,6 @@ export default function Signin() { </Pressable> </View> </TouchableWithoutFeedback> - <CustomHeadersModal - visible={isCustomHeadersModalVisible} - customHeaders={settings.customHeaders || {}} - onClose={() => setIsCustomHeadersModalVisible(false)} - onSave={handleSaveCustomHeaders} - /> </KeyboardAvoidingView> ); } |
