From 0aa3549561e655ce2ae99a38725a7bc50ea6d64d Mon Sep 17 00:00:00 2001 From: m Date: Mon, 13 Jul 2026 11:47:48 +0200 Subject: [PATCH] add tags --- backend/internal/config/config.go | 24 +++--- backend/internal/handler/files.go | 10 ++- backend/internal/service/file.go | 4 +- mobile/.gitignore | 1 + mobile/app/index.tsx | 137 +++++++++++++++++++++++++++++- mobile/hooks/useFiles.ts | 8 +- 6 files changed, 162 insertions(+), 22 deletions(-) diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index b853cfb..c5711b5 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -3,22 +3,22 @@ package config import "os" type Config struct { - Port string - DatabaseURL string - OCREndpoint string - UploadDir string - HMACSecret string - ServerHost string + Port string + DatabaseURL string + OCREndpoint string + UploadDir string + HMACSecret string + ServerHost string } func Load() *Config { return &Config{ - Port: envOr("PORT", "8080"), - DatabaseURL: envOr("DATABASE_URL", "postgres://localhost:5432/vaultdrop?sslmode=disable"), - OCREndpoint: envOr("OCR_ENDPOINT", "http://localhost:9090"), - UploadDir: envOr("UPLOAD_DIR", "./uploads"), - HMACSecret: envOr("HMAC_SECRET", "thisismyrandomstring"), - ServerHost: envOr("SERVER_HOST", "http://192.168.1.17:8080"), + Port: envOr("PORT", "8080"), + DatabaseURL: envOr("DATABASE_URL", "postgres://localhost:5432/vaultdrop?sslmode=disable"), + OCREndpoint: envOr("OCR_ENDPOINT", "http://localhost:9090"), + UploadDir: envOr("UPLOAD_DIR", "./uploads"), + HMACSecret: envOr("HMAC_SECRET", "thisismyrandomstring"), + ServerHost: envOr("SERVER_HOST", "http://192.168.1.17:8080"), } } diff --git a/backend/internal/handler/files.go b/backend/internal/handler/files.go index bba9ca0..dd6556e 100644 --- a/backend/internal/handler/files.go +++ b/backend/internal/handler/files.go @@ -140,14 +140,20 @@ func (h *FileHandler) AddTags(c *gin.Context) { id := c.Param("id") 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 { api.Error(c, http.StatusBadRequest, "INVALID_BODY", "Body must contain a 'tags' array") 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") return } diff --git a/backend/internal/service/file.go b/backend/internal/service/file.go index 2ec4846..0c05fac 100644 --- a/backend/internal/service/file.go +++ b/backend/internal/service/file.go @@ -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 { tag, err := s.queries.GetTagByName(context.Background(), name) if err == sql.ErrNoRows { tag, err = s.queries.CreateTag(context.Background(), db.CreateTagParams{ ID: uuid.New().String(), TagName: name, - TagType: "none", + TagType: tagType, }) if err != nil { return fmt.Errorf("create tag %q: %w", name, err) diff --git a/mobile/.gitignore b/mobile/.gitignore index d914c32..eee5694 100644 --- a/mobile/.gitignore +++ b/mobile/.gitignore @@ -17,6 +17,7 @@ expo-env.d.ts *.p12 *.key *.mobileprovision +.env # Metro .metro-health-check* diff --git a/mobile/app/index.tsx b/mobile/app/index.tsx index 593a161..9a3b41d 100644 --- a/mobile/app/index.tsx +++ b/mobile/app/index.tsx @@ -1,10 +1,10 @@ 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 { useSafeAreaInsets } from 'react-native-safe-area-context'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; 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 { SearchBar, SearchFilters } from '../components/SearchBar'; import { FileThumbnail } from '../components/FileThumbnail'; @@ -135,6 +135,10 @@ export function HomeScreen() { const [keyboardOpen, setKeyboardOpen] = useState(false); const [selectedIds, setSelectedIds] = useState>(new Set()); 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(() => { const show = Keyboard.addListener('keyboardDidShow', () => setKeyboardOpen(true)); @@ -151,6 +155,8 @@ export function HomeScreen() { [files, searchQuery, filters] ); + console.log(filteredFiles.map(f => f.tags)) + const groups = groupByTags ? groupByTag(filteredFiles) : groupByDate(filteredFiles); const groupKeys = Object.keys(groups); @@ -204,6 +210,24 @@ export function HomeScreen() { setSelectedIds(new Set()); }, [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) => { if (selectionMode) { toggleSelection(file.id); @@ -323,6 +347,20 @@ export function HomeScreen() { Éditer + openTagModal('tag')} + > + + Tags + + openTagModal('folder')} + > + + Folder + ) : ( @@ -349,6 +387,38 @@ export function HomeScreen() { )} + + setTagModalVisible(false)}> + setTagModalVisible(false)}> + {}}> + + {tagModalMode === 'folder' ? 'Créer un dossier' : 'Ajouter un tag'} + + + + setTagModalVisible(false)}> + Annuler + + + Ajouter + + + + + ); } @@ -509,4 +579,67 @@ const styles = StyleSheet.create({ fontSize: 15, 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', + }, }); diff --git a/mobile/hooks/useFiles.ts b/mobile/hooks/useFiles.ts index f9368b1..36801d9 100644 --- a/mobile/hooks/useFiles.ts +++ b/mobile/hooks/useFiles.ts @@ -44,10 +44,10 @@ export function useAddTags() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({ fileId, tags }: { fileId: string; tags: string[] }) => - apiClient.post(`${ENDPOINTS.FILES}/${fileId}/tags`, { tags }), - onSuccess: (_, { fileId }) => { - queryClient.invalidateQueries({ queryKey: ['files', fileId] }); + mutationFn: ({ fileId, tags, tagType }: { fileId: string; tags: string[]; tagType?: string }) => + apiClient.post(`${ENDPOINTS.FILES}/${fileId}/tags`, { tags, tag_type: tagType }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['files'] }); }, }); }