This commit is contained in:
m
2026-07-13 11:47:48 +02:00
parent c3bb0dd6ab
commit 0aa3549561
6 changed files with 162 additions and 22 deletions
+12 -12
View File
@@ -3,22 +3,22 @@ package config
import "os" import "os"
type Config struct { type Config struct {
Port string Port string
DatabaseURL string DatabaseURL string
OCREndpoint string OCREndpoint string
UploadDir string UploadDir string
HMACSecret string HMACSecret string
ServerHost string ServerHost string
} }
func Load() *Config { func Load() *Config {
return &Config{ return &Config{
Port: envOr("PORT", "8080"), Port: envOr("PORT", "8080"),
DatabaseURL: envOr("DATABASE_URL", "postgres://localhost:5432/vaultdrop?sslmode=disable"), DatabaseURL: envOr("DATABASE_URL", "postgres://localhost:5432/vaultdrop?sslmode=disable"),
OCREndpoint: envOr("OCR_ENDPOINT", "http://localhost:9090"), OCREndpoint: envOr("OCR_ENDPOINT", "http://localhost:9090"),
UploadDir: envOr("UPLOAD_DIR", "./uploads"), UploadDir: envOr("UPLOAD_DIR", "./uploads"),
HMACSecret: envOr("HMAC_SECRET", "thisismyrandomstring"), HMACSecret: envOr("HMAC_SECRET", "thisismyrandomstring"),
ServerHost: envOr("SERVER_HOST", "http://192.168.1.17:8080"), ServerHost: envOr("SERVER_HOST", "http://192.168.1.17:8080"),
} }
} }
+8 -2
View File
@@ -140,14 +140,20 @@ func (h *FileHandler) AddTags(c *gin.Context) {
id := c.Param("id") id := c.Param("id")
var body struct { var body struct {
Tags []string `json:"tags" binding:"required"` Tags []string `json:"tags" binding:"required"`
TagType string `json:"tag_type"`
} }
if err := c.ShouldBindJSON(&body); err != nil { if err := c.ShouldBindJSON(&body); err != nil {
api.Error(c, http.StatusBadRequest, "INVALID_BODY", "Body must contain a 'tags' array") api.Error(c, http.StatusBadRequest, "INVALID_BODY", "Body must contain a 'tags' array")
return return
} }
if err := h.files.AddTags(id, body.Tags); err != nil { tagType := body.TagType
if tagType == "" {
tagType = "none"
}
if err := h.files.AddTags(id, body.Tags, tagType); err != nil {
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to add tags") api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to add tags")
return return
} }
+2 -2
View File
@@ -122,14 +122,14 @@ func (s *FileService) UpdateOCRText(id, text string) error {
}) })
} }
func (s *FileService) AddTags(fileID string, tagNames []string) error { func (s *FileService) AddTags(fileID string, tagNames []string, tagType string) error {
for _, name := range tagNames { for _, name := range tagNames {
tag, err := s.queries.GetTagByName(context.Background(), name) tag, err := s.queries.GetTagByName(context.Background(), name)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
tag, err = s.queries.CreateTag(context.Background(), db.CreateTagParams{ tag, err = s.queries.CreateTag(context.Background(), db.CreateTagParams{
ID: uuid.New().String(), ID: uuid.New().String(),
TagName: name, TagName: name,
TagType: "none", TagType: tagType,
}) })
if err != nil { if err != nil {
return fmt.Errorf("create tag %q: %w", name, err) return fmt.Errorf("create tag %q: %w", name, err)
+1
View File
@@ -17,6 +17,7 @@ expo-env.d.ts
*.p12 *.p12
*.key *.key
*.mobileprovision *.mobileprovision
.env
# Metro # Metro
.metro-health-check* .metro-health-check*
+135 -2
View File
@@ -1,10 +1,10 @@
import React, { useState, useMemo, useEffect, useCallback } from 'react'; import React, { useState, useMemo, useEffect, useCallback } from 'react';
import { View, FlatList, StyleSheet, TouchableOpacity, Text, Dimensions, KeyboardAvoidingView, Platform, Keyboard, Alert } from 'react-native'; import { View, FlatList, StyleSheet, TouchableOpacity, Text, Dimensions, KeyboardAvoidingView, Platform, Keyboard, Alert, Modal, TextInput } from 'react-native';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
import { useFiles, useFileImage, useDeleteFile } from '../hooks/useFiles'; import { useFiles, useFileImage, useDeleteFile, useAddTags } from '../hooks/useFiles';
import { FileItem } from '../types'; import { FileItem } from '../types';
import { SearchBar, SearchFilters } from '../components/SearchBar'; import { SearchBar, SearchFilters } from '../components/SearchBar';
import { FileThumbnail } from '../components/FileThumbnail'; import { FileThumbnail } from '../components/FileThumbnail';
@@ -135,6 +135,10 @@ export function HomeScreen() {
const [keyboardOpen, setKeyboardOpen] = useState(false); const [keyboardOpen, setKeyboardOpen] = useState(false);
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set()); const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [groupByTags, setGroupByTags] = useState(false); const [groupByTags, setGroupByTags] = useState(false);
const [tagModalVisible, setTagModalVisible] = useState(false);
const [tagModalMode, setTagModalMode] = useState<'tag' | 'folder'>('tag');
const [tagInput, setTagInput] = useState('');
const addTags = useAddTags();
useEffect(() => { useEffect(() => {
const show = Keyboard.addListener('keyboardDidShow', () => setKeyboardOpen(true)); const show = Keyboard.addListener('keyboardDidShow', () => setKeyboardOpen(true));
@@ -151,6 +155,8 @@ export function HomeScreen() {
[files, searchQuery, filters] [files, searchQuery, filters]
); );
console.log(filteredFiles.map(f => f.tags))
const groups = groupByTags ? groupByTag(filteredFiles) : groupByDate(filteredFiles); const groups = groupByTags ? groupByTag(filteredFiles) : groupByDate(filteredFiles);
const groupKeys = Object.keys(groups); const groupKeys = Object.keys(groups);
@@ -204,6 +210,24 @@ export function HomeScreen() {
setSelectedIds(new Set()); setSelectedIds(new Set());
}, [selectedIds, navigation]); }, [selectedIds, navigation]);
const openTagModal = useCallback((mode: 'tag' | 'folder') => {
setTagModalMode(mode);
setTagInput('');
setTagModalVisible(true);
}, []);
const handleAddTag = useCallback(async () => {
const name = tagInput.trim();
if (!name) return;
const ids = Array.from(selectedIds);
const tagType = tagModalMode === 'folder' ? 'folder' : 'none';
for (const id of ids) {
await addTags.mutateAsync({ fileId: id, tags: [name], tagType });
}
setTagModalVisible(false);
setSelectedIds(new Set());
}, [tagInput, selectedIds, tagModalMode, addTags]);
const handleItemPress = useCallback((file: FileItem) => { const handleItemPress = useCallback((file: FileItem) => {
if (selectionMode) { if (selectionMode) {
toggleSelection(file.id); toggleSelection(file.id);
@@ -323,6 +347,20 @@ export function HomeScreen() {
<MaterialIcons name="edit" size={20} color="#1976D2" /> <MaterialIcons name="edit" size={20} color="#1976D2" />
<Text style={[styles.selectionActionText, { color: '#1976D2' }]}>Éditer</Text> <Text style={[styles.selectionActionText, { color: '#1976D2' }]}>Éditer</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity
style={[styles.selectionActionBtn, styles.tagActionBtn]}
onPress={() => openTagModal('tag')}
>
<MaterialIcons name="label" size={20} color="#7B1FA2" />
<Text style={[styles.selectionActionText, { color: '#7B1FA2' }]}>Tags</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.selectionActionBtn, styles.folderActionBtn]}
onPress={() => openTagModal('folder')}
>
<MaterialIcons name="create-new-folder" size={20} color="#F57C00" />
<Text style={[styles.selectionActionText, { color: '#F57C00' }]}>Folder</Text>
</TouchableOpacity>
</View> </View>
</View> </View>
) : ( ) : (
@@ -349,6 +387,38 @@ export function HomeScreen() {
</TouchableOpacity> </TouchableOpacity>
</View> </View>
)} )}
<Modal visible={tagModalVisible} transparent animationType="fade" onRequestClose={() => setTagModalVisible(false)}>
<TouchableOpacity style={styles.modalOverlay} activeOpacity={1} onPress={() => setTagModalVisible(false)}>
<TouchableOpacity activeOpacity={1} style={styles.modalContent} onPress={() => {}}>
<Text style={styles.modalTitle}>
{tagModalMode === 'folder' ? 'Créer un dossier' : 'Ajouter un tag'}
</Text>
<TextInput
style={styles.modalInput}
placeholder={tagModalMode === 'folder' ? 'Nom du dossier...' : 'Nom du tag...'}
placeholderTextColor="#999"
value={tagInput}
onChangeText={setTagInput}
autoFocus
returnKeyType="done"
onSubmitEditing={handleAddTag}
/>
<View style={styles.modalActions}>
<TouchableOpacity style={styles.modalCancelBtn} onPress={() => setTagModalVisible(false)}>
<Text style={styles.modalCancelText}>Annuler</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.modalConfirmBtn, !tagInput.trim() && styles.modalConfirmDisabled]}
onPress={handleAddTag}
disabled={!tagInput.trim()}
>
<Text style={styles.modalConfirmText}>Ajouter</Text>
</TouchableOpacity>
</View>
</TouchableOpacity>
</TouchableOpacity>
</Modal>
</KeyboardAvoidingView> </KeyboardAvoidingView>
); );
} }
@@ -509,4 +579,67 @@ const styles = StyleSheet.create({
fontSize: 15, fontSize: 15,
fontWeight: '600', fontWeight: '600',
}, },
tagActionBtn: {
borderColor: '#7B1FA2',
backgroundColor: 'transparent',
},
folderActionBtn: {
borderColor: '#F57C00',
backgroundColor: 'transparent',
},
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.4)',
justifyContent: 'center',
alignItems: 'center',
},
modalContent: {
backgroundColor: '#fff',
borderRadius: 12,
padding: 20,
width: '80%',
},
modalTitle: {
fontSize: 18,
fontWeight: '700',
color: '#333',
marginBottom: 16,
},
modalInput: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 10,
fontSize: 16,
color: '#333',
marginBottom: 16,
},
modalActions: {
flexDirection: 'row',
justifyContent: 'flex-end',
gap: 12,
},
modalCancelBtn: {
paddingHorizontal: 16,
paddingVertical: 8,
},
modalCancelText: {
fontSize: 15,
color: '#666',
},
modalConfirmBtn: {
paddingHorizontal: 16,
paddingVertical: 8,
backgroundColor: '#1976D2',
borderRadius: 8,
},
modalConfirmDisabled: {
backgroundColor: '#ccc',
},
modalConfirmText: {
fontSize: 15,
color: '#fff',
fontWeight: '600',
},
}); });
+4 -4
View File
@@ -44,10 +44,10 @@ export function useAddTags() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: ({ fileId, tags }: { fileId: string; tags: string[] }) => mutationFn: ({ fileId, tags, tagType }: { fileId: string; tags: string[]; tagType?: string }) =>
apiClient.post(`${ENDPOINTS.FILES}/${fileId}/tags`, { tags }), apiClient.post(`${ENDPOINTS.FILES}/${fileId}/tags`, { tags, tag_type: tagType }),
onSuccess: (_, { fileId }) => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['files', fileId] }); queryClient.invalidateQueries({ queryKey: ['files'] });
}, },
}); });
} }