implement image upload and file management features in ImageScreen
This commit is contained in:
+271
-8
@@ -1,27 +1,290 @@
|
|||||||
import ThemedHeader from "@/src/components/themed/ThemedHeader";
|
import ThemedHeader from "@/src/components/themed/ThemedHeader";
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import ThemedBackground from "@/src/components/themed/ThemedBackground";
|
import ThemedBackground from "@/src/components/themed/ThemedBackground";
|
||||||
import CustomImagePicker from "@/src/components/ImagePicker";
|
import CustomImagePicker from "@/src/components/ImagePicker";
|
||||||
import {ImagePickerSuccessResult} from "expo-image-picker";
|
import { ImagePickerSuccessResult } from "expo-image-picker";
|
||||||
|
import { View, StyleSheet, Alert, FlatList, Text, ActivityIndicator, TouchableOpacity, Linking, Platform } from "react-native";
|
||||||
|
import { S3File, RestService } from "@/src/services/RestService";
|
||||||
|
import ThemedButton from "@/src/components/themed/ThemedButton";
|
||||||
|
import { useTheme } from "@/src/context/ThemeProvider";
|
||||||
|
import { useAuth } from "@/src/context/AuthProvider";
|
||||||
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
|
|
||||||
export default function ImageScreen() {
|
export default function ImageScreen() {
|
||||||
const onSuccess = (result: ImagePickerSuccessResult) => {
|
const { token } = useAuth();
|
||||||
console.log("Image picked successfully", result);
|
const [uploading, setUploading] = useState(false);
|
||||||
}
|
const [files, setFiles] = useState<S3File[]>([]);
|
||||||
|
const [showFiles, setShowFiles] = useState(false);
|
||||||
|
const [loadingFiles, setLoadingFiles] = useState(false);
|
||||||
|
const { theme } = useTheme();
|
||||||
|
|
||||||
|
const { primary, onSurface, outline, error } = theme.colors;
|
||||||
|
|
||||||
|
const fetchStoredFiles = async () => {
|
||||||
|
setLoadingFiles(true);
|
||||||
|
try {
|
||||||
|
const response = await new RestService(token).getStoredFiles();
|
||||||
|
if (response.ok && response.data.files) {
|
||||||
|
setFiles(response.data.files);
|
||||||
|
setShowFiles(true);
|
||||||
|
} else {
|
||||||
|
Alert.alert("Fehler", "Dateien konnten nicht abgerufen werden");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Fehler beim Abrufen der Dateien:", error);
|
||||||
|
Alert.alert("Fehler", "Dateien konnten nicht abgerufen werden");
|
||||||
|
} finally {
|
||||||
|
setLoadingFiles(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSuccess = async (result: ImagePickerSuccessResult) => {
|
||||||
|
if (result.assets && result.assets.length > 0) {
|
||||||
|
const selectedAsset = result.assets[0];
|
||||||
|
console.log("Image picked successfully", selectedAsset);
|
||||||
|
|
||||||
|
try {
|
||||||
|
setUploading(true);
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
const fileInfo = {
|
||||||
|
uri: selectedAsset.uri,
|
||||||
|
type: selectedAsset.mimeType || 'image/jpeg',
|
||||||
|
name: selectedAsset.fileName || 'upload.jpg'
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
formData.append('image', fileInfo);
|
||||||
|
|
||||||
|
const response = await new RestService(token).uploadFile(formData);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
Alert.alert("Erfolg", "Datei erfolgreich hochgeladen");
|
||||||
|
fetchStoredFiles();
|
||||||
|
} else {
|
||||||
|
Alert.alert("Fehler", "Upload fehlgeschlagen");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Fehler beim Hochladen der Datei:", error);
|
||||||
|
Alert.alert("Fehler", "Upload fehlgeschlagen");
|
||||||
|
} finally {
|
||||||
|
setUploading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onFailure = (error: Error) => {
|
const onFailure = (error: Error) => {
|
||||||
console.error("Error picking image", error);
|
console.error("Error picking image", error);
|
||||||
}
|
Alert.alert("Fehler", "Bildauswahl fehlgeschlagen");
|
||||||
|
};
|
||||||
|
|
||||||
const onCanceled = () => {
|
const onCanceled = () => {
|
||||||
console.log("Image picking canceled");
|
console.log("Image picking canceled");
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const toggleFilesList = () => {
|
||||||
|
if (!showFiles) {
|
||||||
|
fetchStoredFiles();
|
||||||
|
} else {
|
||||||
|
setShowFiles(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString: Date) => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleString('de-DE');
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatFileSize = (size: number) => {
|
||||||
|
if (size < 1024) {
|
||||||
|
return `${size} B`;
|
||||||
|
} else if (size < 1024 * 1024) {
|
||||||
|
return `${(size / 1024).toFixed(2)} KB`;
|
||||||
|
} else {
|
||||||
|
return `${(size / (1024 * 1024)).toFixed(2)} MB`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const viewFile = async (objectKey: string, fileName: string, mimeType: string) => {
|
||||||
|
try {
|
||||||
|
const response = await new RestService(token).getFileUrl(objectKey);
|
||||||
|
if (response.ok && response.data.url) {
|
||||||
|
const url = response.data.url;
|
||||||
|
|
||||||
|
const canOpen = await Linking.canOpenURL(url);
|
||||||
|
if (canOpen) {
|
||||||
|
await Linking.openURL(url);
|
||||||
|
} else {
|
||||||
|
Alert.alert("Fehler", "Diese URL kann nicht geöffnet werden");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Alert.alert("Fehler", "Datei-URL konnte nicht abgerufen werden");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Fehler beim Abrufen der Datei-URL:", error);
|
||||||
|
Alert.alert("Fehler", "Datei-URL konnte nicht abgerufen werden");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteFile = async (objectKey: string) => {
|
||||||
|
Alert.alert(
|
||||||
|
"Datei löschen",
|
||||||
|
"Möchtest du diese Datei wirklich löschen?",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: "Abbrechen",
|
||||||
|
style: "cancel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Löschen",
|
||||||
|
style: "destructive",
|
||||||
|
onPress: async () => {
|
||||||
|
try {
|
||||||
|
const response = await new RestService(token).deleteFile(objectKey);
|
||||||
|
if (response.ok) {
|
||||||
|
Alert.alert("Erfolg", "Datei erfolgreich gelöscht");
|
||||||
|
// Liste aktualisieren
|
||||||
|
fetchStoredFiles();
|
||||||
|
} else {
|
||||||
|
Alert.alert("Fehler", "Datei konnte nicht gelöscht werden");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Fehler beim Löschen der Datei:", error);
|
||||||
|
Alert.alert("Fehler", "Datei konnte nicht gelöscht werden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemedBackground>
|
<ThemedBackground>
|
||||||
<ThemedHeader>
|
<ThemedHeader>
|
||||||
Bildschirm für Bildauswahl
|
Bildschirm für Bildauswahl
|
||||||
</ThemedHeader>
|
</ThemedHeader>
|
||||||
<CustomImagePicker onSuccess={onSuccess} onFailure={onFailure} onCanceled={onCanceled}/>
|
|
||||||
|
<View style={styles.container}>
|
||||||
|
{uploading ? (
|
||||||
|
<View style={styles.loadingContainer}>
|
||||||
|
<ActivityIndicator size="large" color={primary} />
|
||||||
|
<Text style={[styles.loadingText, { color: onSurface }]}>
|
||||||
|
Datei wird hochgeladen...
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<CustomImagePicker
|
||||||
|
onSuccess={onSuccess}
|
||||||
|
onFailure={onFailure}
|
||||||
|
onCanceled={onCanceled}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View style={styles.buttonContainer}>
|
||||||
|
<ThemedButton
|
||||||
|
onPress={toggleFilesList}
|
||||||
|
title={showFiles ? "Dateien ausblenden" : "Gespeicherte Dateien anzeigen"}
|
||||||
|
mode="contained"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{showFiles && (
|
||||||
|
<View style={styles.filesList}>
|
||||||
|
<ThemedHeader>Gespeicherte Dateien</ThemedHeader>
|
||||||
|
|
||||||
|
{loadingFiles ? (
|
||||||
|
<ActivityIndicator size="large" color={primary} />
|
||||||
|
) : files.length > 0 ? (
|
||||||
|
<FlatList
|
||||||
|
data={files}
|
||||||
|
keyExtractor={(item) => item.key}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<View style={[styles.fileItem, { borderColor: outline }]}>
|
||||||
|
<Text style={{ color: onSurface, fontWeight: 'bold' }}>
|
||||||
|
{item.originalName}
|
||||||
|
</Text>
|
||||||
|
<Text style={{ color: onSurface }}>
|
||||||
|
Typ: {item.mimeType}
|
||||||
|
</Text>
|
||||||
|
<Text style={{ color: onSurface }}>
|
||||||
|
Größe: {formatFileSize(item.size)}
|
||||||
|
</Text>
|
||||||
|
<Text style={{ color: onSurface }}>
|
||||||
|
Zuletzt geändert: {formatDate(item.lastModified)}
|
||||||
|
</Text>
|
||||||
|
<View style={styles.fileItemButtons}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.fileButton, { backgroundColor: primary }]}
|
||||||
|
onPress={() => viewFile(item.key, item.originalName, item.mimeType)}
|
||||||
|
>
|
||||||
|
<MaterialIcons name="visibility" size={24} color="white" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.fileButton, { backgroundColor: error }]}
|
||||||
|
onPress={() => deleteFile(item.key)}
|
||||||
|
>
|
||||||
|
<MaterialIcons name="delete" size={24} color="white" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text style={{ color: onSurface, textAlign: 'center' }}>
|
||||||
|
Keine Dateien gefunden
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
</ThemedBackground>
|
</ThemedBackground>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
width: '100%',
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
loadingContainer: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
loadingText: {
|
||||||
|
marginTop: 10,
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
|
buttonContainer: {
|
||||||
|
marginVertical: 20,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
filesList: {
|
||||||
|
flex: 1,
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
fileItem: {
|
||||||
|
padding: 12,
|
||||||
|
marginVertical: 8,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 8,
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
|
fileItemButtons: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
position: 'absolute',
|
||||||
|
top: 12,
|
||||||
|
right: 12,
|
||||||
|
},
|
||||||
|
fileButton: {
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
borderRadius: 18,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginLeft: 8,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -18,6 +18,14 @@ export interface ApiResponse<T> {
|
|||||||
data: T;
|
data: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface S3File {
|
||||||
|
key: string;
|
||||||
|
lastModified: Date;
|
||||||
|
originalName: string;
|
||||||
|
mimeType: string;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
class RestService {
|
class RestService {
|
||||||
private readonly jwtToken: string | null;
|
private readonly jwtToken: string | null;
|
||||||
private api: AxiosInstance;
|
private api: AxiosInstance;
|
||||||
@@ -148,6 +156,36 @@ class RestService {
|
|||||||
return this.request<ApiResponse<{ message: string }>>('POST', '/auth/logout');
|
return this.request<ApiResponse<{ message: string }>>('POST', '/auth/logout');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async uploadFile(file: FormData): Promise<ApiResponse<{ message: string, objectKey: string }>> {
|
||||||
|
return this.request<ApiResponse<{ message: string, objectKey: string }>>(
|
||||||
|
'POST',
|
||||||
|
' /storage/upload',
|
||||||
|
file,
|
||||||
|
{'Content-Type': 'multipart/form-data'}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStoredFiles(): Promise<ApiResponse<{ files: S3File[] }>> {
|
||||||
|
return this.request<ApiResponse<{ files: S3File[] }>>(
|
||||||
|
'GET',
|
||||||
|
'/storage/files'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFileUrl(objectKey: string): Promise<ApiResponse<{ url: string }>> {
|
||||||
|
return this.request<ApiResponse<{ url: string }>>(
|
||||||
|
'GET',
|
||||||
|
`/storage/files/${objectKey}/url`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteFile(objectKey: string): Promise<ApiResponse<{ message: string }>> {
|
||||||
|
return this.request<ApiResponse<{ message: string }>>(
|
||||||
|
'DELETE',
|
||||||
|
`/storage/file/${objectKey}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private async request<T>(method: Method, url: string, data?: any, headers?: any): Promise<T> {
|
private async request<T>(method: Method, url: string, data?: any, headers?: any): Promise<T> {
|
||||||
try {
|
try {
|
||||||
const response = await this.api.request<T>({
|
const response = await this.api.request<T>({
|
||||||
|
|||||||
Reference in New Issue
Block a user