342 lines
9.6 KiB
TypeScript
342 lines
9.6 KiB
TypeScript
import { Ionicons } from "@expo/vector-icons";
|
|
import * as Calendar from "expo-calendar";
|
|
import * as SQLite from "expo-sqlite";
|
|
import React, { useEffect, useState } from "react";
|
|
import {
|
|
Button,
|
|
FlatList,
|
|
Modal,
|
|
SafeAreaView,
|
|
StyleSheet,
|
|
Text,
|
|
TextInput,
|
|
TouchableOpacity,
|
|
View,
|
|
} from "react-native";
|
|
|
|
export interface EventItem {
|
|
id: string;
|
|
title: string;
|
|
startDate: string;
|
|
endDate: string;
|
|
}
|
|
|
|
const db = SQLite.openDatabaseSync("events.db");
|
|
|
|
export default function CalendarTab() {
|
|
const [source, setSource] = useState<"sqlite" | "iphone">("sqlite");
|
|
const [events, setEvents] = useState<EventItem[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [selectedYear, setSelectedYear] = useState<number>(
|
|
new Date().getFullYear()
|
|
);
|
|
const [permissionError, setPermissionError] = useState<string | null>(null);
|
|
const [calendarType, setCalendarType] = useState<"calendar" | "reminder">(
|
|
"calendar"
|
|
);
|
|
const [modalVisible, setModalVisible] = useState(false);
|
|
const [newTitle, setNewTitle] = useState("");
|
|
const [newStart, setNewStart] = useState("");
|
|
const [newEnd, setNewEnd] = useState("");
|
|
|
|
useEffect(() => {
|
|
if (source === "sqlite") {
|
|
loadEventsFromDB();
|
|
} else {
|
|
loadEventsFromDevice();
|
|
}
|
|
}, [source, selectedYear, calendarType]);
|
|
|
|
const loadEventsFromDB = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const stmt = await db.prepareAsync("SELECT * FROM events;");
|
|
const loadedEvents: EventItem[] = [];
|
|
const result = await stmt.executeAsync([]);
|
|
for await (const row of result) {
|
|
loadedEvents.push(row as EventItem);
|
|
}
|
|
setEvents(loadedEvents);
|
|
await stmt.finalizeAsync();
|
|
} catch (e) {
|
|
setEvents([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const loadEventsFromDevice = async () => {
|
|
setLoading(true);
|
|
setPermissionError(null);
|
|
try {
|
|
let perm;
|
|
if (calendarType === "reminder") {
|
|
perm = await Calendar.requestRemindersPermissionsAsync();
|
|
} else {
|
|
perm = await Calendar.requestCalendarPermissionsAsync();
|
|
}
|
|
if (!perm.granted) {
|
|
setEvents([]);
|
|
setPermissionError(
|
|
`Keine Berechtigung für ${
|
|
calendarType === "reminder" ? "Erinnerungen" : "Kalender"
|
|
}-Zugriff. Bitte in den iOS-Einstellungen aktivieren.`
|
|
);
|
|
return;
|
|
}
|
|
let calendars = await Calendar.getCalendarsAsync();
|
|
console.log(
|
|
"Gefundene Kalender:",
|
|
calendars.map((c) => ({ id: c.id, title: c.title, type: c.type }))
|
|
);
|
|
if (calendarType === "calendar") {
|
|
calendars = calendars.filter((c) => String(c.type) === "calendar");
|
|
} else {
|
|
calendars = calendars.filter((c) => String(c.type) === "reminder");
|
|
}
|
|
const start = new Date(selectedYear, 0, 1);
|
|
const end = new Date(selectedYear, 11, 31, 23, 59, 59);
|
|
let allEvents: EventItem[] = [];
|
|
for (const cal of calendars) {
|
|
const calEvents = await Calendar.getEventsAsync([cal.id], start, end);
|
|
console.log(
|
|
`Events für Kalender ${cal.title}:`,
|
|
calEvents.map((e) => ({
|
|
id: e.id,
|
|
title: e.title,
|
|
start: e.startDate,
|
|
end: e.endDate,
|
|
}))
|
|
);
|
|
allEvents = allEvents.concat(
|
|
calEvents.map((e) => ({
|
|
id: e.id,
|
|
title: e.title || "(Kein Titel)",
|
|
startDate:
|
|
typeof e.startDate === "string"
|
|
? e.startDate
|
|
: new Date(e.startDate).toISOString(),
|
|
endDate:
|
|
typeof e.endDate === "string"
|
|
? e.endDate
|
|
: new Date(e.endDate).toISOString(),
|
|
}))
|
|
);
|
|
}
|
|
setEvents(allEvents);
|
|
} catch (e: any) {
|
|
setEvents([]);
|
|
setPermissionError(
|
|
"Fehler beim Zugriff auf " +
|
|
(calendarType === "reminder" ? "Erinnerungen" : "Kalender") +
|
|
": " +
|
|
(e?.message || JSON.stringify(e))
|
|
);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const addEventToDB = async () => {
|
|
if (!newTitle || !newStart || !newEnd) return;
|
|
await db.execAsync(
|
|
`INSERT INTO events (id, title, startDate, endDate) VALUES ('${Date.now()}', '${newTitle.replace(
|
|
/'/g,
|
|
"''"
|
|
)}', '${newStart}', '${newEnd}');`
|
|
);
|
|
setModalVisible(false);
|
|
setNewTitle("");
|
|
setNewStart("");
|
|
setNewEnd("");
|
|
await loadEventsFromDB();
|
|
};
|
|
|
|
const renderItem = ({ item }: { item: EventItem }) => (
|
|
<View style={styles.item}>
|
|
<Text style={styles.title}>{item.title}</Text>
|
|
<Text style={styles.time}>
|
|
{new Date(item.startDate).toLocaleString()} -{" "}
|
|
{new Date(item.endDate).toLocaleString()}
|
|
</Text>
|
|
</View>
|
|
);
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container}>
|
|
<View
|
|
style={{
|
|
flexDirection: "row",
|
|
justifyContent: "flex-end",
|
|
alignItems: "center",
|
|
}}
|
|
>
|
|
<TouchableOpacity
|
|
onPress={() => setModalVisible(true)}
|
|
style={{ padding: 10 }}
|
|
>
|
|
<Ionicons name="add-circle" size={32} color="#007AFF" />
|
|
</TouchableOpacity>
|
|
</View>
|
|
<View style={styles.switchRow}>
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.switchButton,
|
|
source === "sqlite" && styles.activeButton,
|
|
]}
|
|
onPress={() => setSource("sqlite")}
|
|
>
|
|
<Text style={styles.switchText}>SQLite</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.switchButton,
|
|
source === "iphone" && styles.activeButton,
|
|
]}
|
|
onPress={() => setSource("iphone")}
|
|
>
|
|
<Text style={styles.switchText}>
|
|
iPhone ({calendarType === "calendar" ? "Kalender" : "Erinnerungen"})
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
{source === "iphone" && (
|
|
<View style={styles.switchRow}>
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.switchButton,
|
|
calendarType === "calendar" && styles.activeButton,
|
|
]}
|
|
onPress={() => setCalendarType("calendar")}
|
|
>
|
|
<Text style={styles.switchText}>Kalender</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.switchButton,
|
|
calendarType === "reminder" && styles.activeButton,
|
|
]}
|
|
onPress={() => setCalendarType("reminder")}
|
|
>
|
|
<Text style={styles.switchText}>Erinnerungen</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
)}
|
|
<Text style={styles.header}>Kalender Events ({selectedYear})</Text>
|
|
<FlatList
|
|
data={events}
|
|
keyExtractor={(item) => item.id}
|
|
renderItem={renderItem}
|
|
ListEmptyComponent={
|
|
permissionError ? (
|
|
<Text style={{ textAlign: "center", color: "red" }}>
|
|
{permissionError}
|
|
</Text>
|
|
) : (
|
|
<Text style={{ textAlign: "center" }}>Keine Events gefunden.</Text>
|
|
)
|
|
}
|
|
refreshing={loading}
|
|
onRefresh={
|
|
source === "sqlite" ? loadEventsFromDB : loadEventsFromDevice
|
|
}
|
|
/>
|
|
<Modal
|
|
visible={modalVisible}
|
|
animationType="slide"
|
|
transparent={true}
|
|
onRequestClose={() => setModalVisible(false)}
|
|
>
|
|
<View style={styles.modalOverlay}>
|
|
<View style={styles.modalContent}>
|
|
<Text style={styles.header}>Neuer SQLite-Eintrag</Text>
|
|
<TextInput
|
|
style={styles.input}
|
|
placeholder="Titel"
|
|
value={newTitle}
|
|
onChangeText={setNewTitle}
|
|
/>
|
|
<TextInput
|
|
style={styles.input}
|
|
placeholder="Start (YYYY-MM-DD HH:mm)"
|
|
value={newStart}
|
|
onChangeText={setNewStart}
|
|
/>
|
|
<TextInput
|
|
style={styles.input}
|
|
placeholder="Ende (YYYY-MM-DD HH:mm)"
|
|
value={newEnd}
|
|
onChangeText={setNewEnd}
|
|
/>
|
|
<View
|
|
style={{
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
marginTop: 10,
|
|
}}
|
|
>
|
|
<Button
|
|
title="Abbrechen"
|
|
onPress={() => setModalVisible(false)}
|
|
/>
|
|
<Button title="Speichern" onPress={addEventToDB} />
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: "#fff", padding: 10 },
|
|
header: {
|
|
fontSize: 20,
|
|
fontWeight: "bold",
|
|
marginBottom: 16,
|
|
textAlign: "center",
|
|
},
|
|
item: {
|
|
marginBottom: 12,
|
|
padding: 12,
|
|
borderRadius: 8,
|
|
backgroundColor: "#f2f2f2",
|
|
},
|
|
title: { fontSize: 16, fontWeight: "600" },
|
|
time: { fontSize: 14, color: "#555" },
|
|
switchRow: {
|
|
flexDirection: "row",
|
|
justifyContent: "center",
|
|
marginBottom: 16,
|
|
},
|
|
switchButton: {
|
|
padding: 10,
|
|
borderRadius: 8,
|
|
backgroundColor: "#eee",
|
|
marginHorizontal: 8,
|
|
},
|
|
activeButton: { backgroundColor: "#007AFF" },
|
|
switchText: { color: "#333", fontWeight: "bold" },
|
|
modalOverlay: {
|
|
flex: 1,
|
|
backgroundColor: "rgba(0,0,0,0.2)",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
modalContent: {
|
|
width: 320,
|
|
backgroundColor: "#fff",
|
|
borderRadius: 12,
|
|
padding: 20,
|
|
elevation: 8,
|
|
},
|
|
input: {
|
|
borderWidth: 1,
|
|
borderColor: "#ccc",
|
|
borderRadius: 8,
|
|
padding: 10,
|
|
marginBottom: 12,
|
|
fontSize: 16,
|
|
},
|
|
});
|