diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index 3adb62e..b86e7e3 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -3,12 +3,12 @@ package config import "os" type Config struct { - Port string - DBPath string - OCREndpoint string - UploadDir string - HMACSecret string - ServerHost string + Port string + DBPath string + OCREndpoint string + UploadDir string + HMACSecret string + ServerHost string } func Load() *Config { @@ -18,7 +18,7 @@ func Load() *Config { OCREndpoint: envOr("OCR_ENDPOINT", "http://localhost:9090"), UploadDir: envOr("UPLOAD_DIR", "./uploads"), HMACSecret: envOr("HMAC_SECRET", "thisismyrandomstring"), - ServerHost: envOr("SERVER_HOST", "http://localhost:8080"), + ServerHost: envOr("SERVER_HOST", "http://192.168.1.17:8080"), } } diff --git a/backend/internal/db/files.sql.go b/backend/internal/db/files.sql.go index 24a44c5..27c54d1 100644 --- a/backend/internal/db/files.sql.go +++ b/backend/internal/db/files.sql.go @@ -118,6 +118,45 @@ func (q *Queries) ListFiles(ctx context.Context) ([]File, error) { return items, nil } +const listFilesByID = `-- name: ListFilesByID :many +SELECT id, name, mime_type, size, storage_key, checksum, ocr_text, created_at, updated_at FROM files +WHERE id IN (SELECT value FROM json_each(?)) +ORDER BY created_at DESC +` + +func (q *Queries) ListFilesByID(ctx context.Context, jsonEach interface{}) ([]File, error) { + rows, err := q.db.QueryContext(ctx, listFilesByID, jsonEach) + if err != nil { + return nil, err + } + defer rows.Close() + var items []File + for rows.Next() { + var i File + if err := rows.Scan( + &i.ID, + &i.Name, + &i.MimeType, + &i.Size, + &i.StorageKey, + &i.Checksum, + &i.OcrText, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const updateFile = `-- name: UpdateFile :exec UPDATE files SET name = ?, mime_type = ?, ocr_text = ?, updated_at = CURRENT_TIMESTAMP diff --git a/backend/internal/db/queries/files.sql b/backend/internal/db/queries/files.sql index bd64651..e97d13d 100644 --- a/backend/internal/db/queries/files.sql +++ b/backend/internal/db/queries/files.sql @@ -6,6 +6,11 @@ WHERE id = ? LIMIT 1; SELECT * FROM files ORDER BY created_at DESC; +-- name: ListFilesByID :many +SELECT * FROM files +WHERE id IN (SELECT value FROM json_each(?)) +ORDER BY created_at DESC; + -- name: CreateFile :one INSERT INTO files (id, name, mime_type, size, storage_key, checksum, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) diff --git a/backend/internal/handler/files.go b/backend/internal/handler/files.go index 6ecd203..6434027 100644 --- a/backend/internal/handler/files.go +++ b/backend/internal/handler/files.go @@ -60,21 +60,38 @@ func (h *FileHandler) List(c *gin.Context) { } type fileResponse struct { - ID string `json:"id"` - URL string `json:"url"` - Name string `json:"name"` - Size int64 `json:"size"` - Tags []string `json:"tags"` + ID string `json:"id"` + URL string `json:"url"` + Name string `json:"name"` + Size int64 `json:"size"` + Tags []string `json:"tags"` + CreatedAt string `json:"createdAt"` } resp := make([]fileResponse, len(files)) for i, f := range files { + + metadata, err := h.files.Get(f.ID) + if err != nil { + + resp[i] = fileResponse{ + ID: f.ID, + URL: h.urls.GenerateDownloadURL(f.ID), + Name: f.Name, + Size: f.Size, + Tags: []string{}, + } + continue + + } + resp[i] = fileResponse{ - ID: f.ID, - URL: h.urls.GenerateDownloadURL(f.ID), - Name: f.Name, - Size: f.Size, - Tags: []string{}, + ID: f.ID, + URL: h.urls.GenerateDownloadURL(f.ID), + Name: f.Name, + Size: f.Size, + Tags: []string{}, + CreatedAt: metadata.CreatedAt, } } diff --git a/backend/internal/service/url.go b/backend/internal/service/url.go index 37b5d32..ccbe728 100644 --- a/backend/internal/service/url.go +++ b/backend/internal/service/url.go @@ -29,7 +29,7 @@ func (s *URLService) GenerateDownloadURL(fileUUID string) string { sig := s.sign(fileUUID, expires) return fmt.Sprintf( - "%s/api/v1/files/%s?expires=%d&sig=%s", + "%s/api/v1/files/download/%s?expires=%d&sig=%s", s.serverHost, fileUUID, expires, diff --git a/mobile/app/index.tsx b/mobile/app/index.tsx index 724613c..fd7c18a 100644 --- a/mobile/app/index.tsx +++ b/mobile/app/index.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import { View, FlatList, StyleSheet, TouchableOpacity, Text, Dimensions, Image } from 'react-native'; +import { View, FlatList, StyleSheet, TouchableOpacity, Text, Dimensions, Image, ActivityIndicator } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; -import { useFiles } from '../hooks/useFiles'; +import { useFiles, useFileImage } from '../hooks/useFiles'; import { FileItem } from '../types'; const NUM_COLUMNS = 3; @@ -20,10 +20,18 @@ type NavigationProp = NativeStackNavigationProp; type GroupedFiles = Record; +function parseBackendDate(dateStr: string): Date | null { + if (!dateStr) return null; + const match = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})/); + if (!match) return new Date(dateStr); + return new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]), Number(match[4]), Number(match[5]), Number(match[6])); +} + function groupByDate(files: FileItem[]): GroupedFiles { const groups: GroupedFiles = {}; for (const file of files) { - const d = new Date(file.createdAt); + const d = parseBackendDate(file.createdAt); + if (!d) continue; const key = d.toLocaleDateString('fr-FR', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', }); @@ -80,16 +88,7 @@ export function HomeScreen() { {formatDateLabel(dateKey)} {groupFiles.map((file) => ( - - {file.url ? ( - - ) : ( - - PDF - - )} - {file.name} - + ))} @@ -123,6 +122,29 @@ export function HomeScreen() { ); } +function FileGridItem({ file }: { file: FileItem }) { + const { data, isLoading } = useFileImage(file.id); + + const uri = data?.data?.url; + + return ( + + {isLoading ? ( + + + + ) : uri ? ( + + ) : ( + + PDF + + )} + {file.name} + + ); +} + const styles = StyleSheet.create({ container: { flex: 1, diff --git a/mobile/hooks/useFiles.ts b/mobile/hooks/useFiles.ts index 53227ae..f9368b1 100644 --- a/mobile/hooks/useFiles.ts +++ b/mobile/hooks/useFiles.ts @@ -21,6 +21,14 @@ export function useFile(id: string) { }); } +export function useFileImage(fileId: string) { + return useQuery({ + queryKey: ['fileImage', fileId], + queryFn: () => apiClient.get<{ data: { id: string; name: string; url: string; size: number } }>(`${ENDPOINTS.FILE}/${fileId}`), + enabled: !!fileId, + }); +} + export function useDeleteFile() { const queryClient = useQueryClient();