feat: add visual feedback for CalDAV save & sync actions
- Show spinner + loading text while request is in progress - Display success (green) or error (red) message, auto-clears after 3s - Save and Sync have independent feedback rows (both visible at once) - Fix CaldavTextInput theming and add secureTextEntry for password - Reset CustomTextInput cursor to start when unfocused
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Text, View } from "react-native";
|
||||
import { ActivityIndicator, Text, View } from "react-native";
|
||||
import BaseBackground from "../../components/BaseBackground";
|
||||
import BaseButton, { BaseButtonProps } from "../../components/BaseButton";
|
||||
import { useThemeStore } from "../../stores/ThemeStore";
|
||||
@@ -8,7 +8,7 @@ import { Ionicons } from "@expo/vector-icons";
|
||||
import { SimpleHeader } from "../../components/Header";
|
||||
import { THEMES } from "../../Themes";
|
||||
import CustomTextInput from "../../components/CustomTextInput";
|
||||
import { useState } from "react";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { CaldavConfigService } from "../../services/CaldavConfigService";
|
||||
import { useCaldavConfigStore } from "../../stores";
|
||||
|
||||
@@ -33,25 +33,54 @@ type CaldavTextInputProps = {
|
||||
title: string;
|
||||
value: string;
|
||||
onValueChange: (text: string) => void;
|
||||
secureTextEntry?: boolean;
|
||||
};
|
||||
|
||||
const CaldavTextInput = ({
|
||||
title,
|
||||
value,
|
||||
onValueChange,
|
||||
secureTextEntry,
|
||||
}: CaldavTextInputProps) => {
|
||||
const { theme } = useThemeStore();
|
||||
return (
|
||||
<View className="flex flex-row items-center py-1">
|
||||
<Text className="ml-4 w-24">{title}:</Text>
|
||||
<Text className="ml-4 w-24" style={{ color: theme.textPrimary }}>{title}:</Text>
|
||||
<CustomTextInput
|
||||
className="flex-1 mr-4 px-3 py-2"
|
||||
text={value}
|
||||
onValueChange={onValueChange}
|
||||
secureTextEntry={secureTextEntry}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
type Feedback = { text: string; isError: boolean; loading: boolean };
|
||||
|
||||
const FeedbackRow = ({ feedback }: { feedback: Feedback | null }) => {
|
||||
const { theme } = useThemeStore();
|
||||
if (!feedback) return null;
|
||||
return (
|
||||
<View className="flex flex-row items-center justify-center mt-2 mx-4 gap-2">
|
||||
{feedback.loading && (
|
||||
<ActivityIndicator size="small" color={theme.textMuted} />
|
||||
)}
|
||||
<Text
|
||||
style={{
|
||||
color: feedback.loading
|
||||
? theme.textMuted
|
||||
: feedback.isError
|
||||
? theme.rejectButton
|
||||
: theme.confirmButton,
|
||||
}}
|
||||
>
|
||||
{feedback.text}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const CaldavSettings = () => {
|
||||
const { theme } = useThemeStore();
|
||||
const { config, setConfig } = useCaldavConfigStore();
|
||||
@@ -59,18 +88,51 @@ const CaldavSettings = () => {
|
||||
const [serverUrl, setServerUrl] = useState(config?.serverUrl ?? "");
|
||||
const [username, setUsername] = useState(config?.username ?? "");
|
||||
const [password, setPassword] = useState(config?.password ?? "");
|
||||
const [saveFeedback, setSaveFeedback] = useState<Feedback | null>(null);
|
||||
const [syncFeedback, setSyncFeedback] = useState<Feedback | null>(null);
|
||||
const saveTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const syncTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const showFeedback = useCallback(
|
||||
(
|
||||
setter: typeof setSaveFeedback,
|
||||
timer: typeof saveTimer,
|
||||
text: string,
|
||||
isError: boolean,
|
||||
loading = false,
|
||||
) => {
|
||||
if (timer.current) clearTimeout(timer.current);
|
||||
setter({ text, isError, loading });
|
||||
if (!loading) {
|
||||
timer.current = setTimeout(() => setter(null), 3000);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const saveConfig = async () => {
|
||||
const saved = await CaldavConfigService.saveConfig(
|
||||
serverUrl,
|
||||
username,
|
||||
password,
|
||||
);
|
||||
setConfig(saved);
|
||||
showFeedback(setSaveFeedback, saveTimer, "Speichere Konfiguration...", false, true);
|
||||
try {
|
||||
const saved = await CaldavConfigService.saveConfig(
|
||||
serverUrl,
|
||||
username,
|
||||
password,
|
||||
);
|
||||
setConfig(saved);
|
||||
showFeedback(setSaveFeedback, saveTimer, "Konfiguration wurde gespeichert", false);
|
||||
} catch {
|
||||
showFeedback(setSaveFeedback, saveTimer, "Fehler beim Speichern der Konfiguration", true);
|
||||
}
|
||||
};
|
||||
|
||||
const sync = async () => {
|
||||
await CaldavConfigService.sync();
|
||||
showFeedback(setSyncFeedback, syncTimer, "Synchronisiere...", false, true);
|
||||
try {
|
||||
await CaldavConfigService.sync();
|
||||
showFeedback(setSyncFeedback, syncTimer, "Synchronisierung erfolgreich", false);
|
||||
} catch {
|
||||
showFeedback(setSyncFeedback, syncTimer, "Fehler beim Synchronisieren", true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -99,6 +161,7 @@ const CaldavSettings = () => {
|
||||
title="password"
|
||||
value={password}
|
||||
onValueChange={setPassword}
|
||||
secureTextEntry
|
||||
/>
|
||||
</View>
|
||||
<View className="flex flex-row">
|
||||
@@ -109,6 +172,8 @@ const CaldavSettings = () => {
|
||||
Sync
|
||||
</BaseButton>
|
||||
</View>
|
||||
<FeedbackRow feedback={saveFeedback} />
|
||||
<FeedbackRow feedback={syncFeedback} />
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -30,6 +30,7 @@ const CustomTextInput = (props: CustomTextInputProps) => {
|
||||
secureTextEntry={props.secureTextEntry}
|
||||
autoCapitalize={props.autoCapitalize}
|
||||
keyboardType={props.keyboardType}
|
||||
selection={!focused ? { start: 0 } : undefined}
|
||||
style={{
|
||||
backgroundColor: theme.messageBorderBg,
|
||||
color: theme.textPrimary,
|
||||
|
||||
Reference in New Issue
Block a user