init project
This commit is contained in:
@@ -0,0 +1,52 @@
|
|||||||
|
# VaultDrop
|
||||||
|
|
||||||
|
## Project Status
|
||||||
|
|
||||||
|
Project initialized — `webui/` (React Native) and `backend/` (Go) have scaffolding in place.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- **Backend**: Go, Gin HTTP framework, SQLite + FTS5, Tesseract OCR (system call)
|
||||||
|
- **Frontend**: React Native (Expo SDK 57), React Navigation, TanStack Query, react-native-image-picker
|
||||||
|
|
||||||
|
## Key Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backend
|
||||||
|
cd backend && go run cmd/server/main.go
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
cd webui && npx expo start
|
||||||
|
|
||||||
|
# Typecheck frontend
|
||||||
|
cd webui && npx tsc --noEmit
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backend Structure
|
||||||
|
|
||||||
|
- Entry point: `backend/cmd/server/main.go`
|
||||||
|
- Internal packages: `handlers/`, `models/`, `repository/`, `service/`, `ocr/`
|
||||||
|
- Response helpers: `pkg/api/response.go`
|
||||||
|
- File uploads stored in `backend/uploads/`
|
||||||
|
- Standard JSON response envelope: `{ "data": ..., "meta": { "page": ..., "total": ... } }`
|
||||||
|
- Error format: `{ "error": { "code": "...", "message": "..." } }`
|
||||||
|
- OCR language: `fra+eng`
|
||||||
|
- Handlers are stub implementations (return not-implemented errors)
|
||||||
|
|
||||||
|
## Frontend Structure
|
||||||
|
|
||||||
|
- Entry point: `webui/App.tsx` (React Navigation + TanStack Query providers)
|
||||||
|
- Screens: `app/index.tsx` (home), `app/upload.tsx`, `app/scan.tsx`, `app/search.tsx`
|
||||||
|
- Components: `components/FileCard.tsx`, `TagChip.tsx`, `UploadProgress.tsx`
|
||||||
|
- Hooks: `hooks/useFiles.ts`, `hooks/useSearch.ts`, `hooks/useUpload.ts`
|
||||||
|
- API client: `api/client.ts`, types: `types/index.ts`, constants: `constants/api.ts`
|
||||||
|
- No business logic on the client — all heavy processing server-side
|
||||||
|
- API base URL configured via `EXPO_PUBLIC_API_BASE_URL` env var
|
||||||
|
|
||||||
|
## Non-Goals (V1)
|
||||||
|
|
||||||
|
Collaborative/multi-user, plugin system, complex offline sync, public sharing.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- `README.md` — full spec, API endpoints, data flow, folder structure, iteration roadmap
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
# VaultDrop — Application de Gestion de Fichiers V1
|
||||||
|
|
||||||
|
## Vision
|
||||||
|
|
||||||
|
Application mobile tout-en-un permettant de centraliser, organiser et retrouver ses documents. Upload depuis l'appareil, scan caméra avec OCR, tagging et recherche rapide. Le back-end Go assure le traitement asynchrone (OCR, indexing) et la persistence. Le front React Native reste léger : il affiche, interagit et met en cache.
|
||||||
|
|
||||||
|
## Scope V1
|
||||||
|
|
||||||
|
On livre un produit fonctionnel utilisable au quotidien, pas un framework. V1 = gestion de fichiers avec scan OCR intégré. Pas de mode collaboratif, pas de plugins, pas de partage avancé.
|
||||||
|
|
||||||
|
## Non-Goals (V1)
|
||||||
|
|
||||||
|
- Mode collaboratif / multi-utilisateur
|
||||||
|
- Système de plugins
|
||||||
|
- Synchronisation offline complexe
|
||||||
|
- Plateforme de partage publique
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Vue d'Ensemble Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────┐ ┌─────────────────────┐
|
||||||
|
│ React Native App │ ────> │ API Go Backend │
|
||||||
|
│ (Expo managed) │ HTTPS │ (REST) │
|
||||||
|
│ - Upload │ │ - Upload fichiers │
|
||||||
|
│ - Caméra / Scan │ │ - OCR async │
|
||||||
|
│ - Tags / Recherche│ │ - Index/search │
|
||||||
|
│ - Affichage │ │ - Persistance │
|
||||||
|
└─────────────────────┘ └─────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Le mobile ne contient pas de logique métier. Tout traitement lourd (OCR, extraction de texte, indexation) est géré côté back-end.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fonctionnalités Attendues
|
||||||
|
|
||||||
|
### Upload de fichiers
|
||||||
|
- Sélection depuis la galerie appareil
|
||||||
|
- Upload par drag-and-drop interne
|
||||||
|
- Support des formats : PDF, images (JPG, PNG)
|
||||||
|
- Feedback visuel pendant l'envoi (progress)
|
||||||
|
- Retry automatique en cas d'échec réseau
|
||||||
|
|
||||||
|
### Scan Documents (Caméra)
|
||||||
|
- Prise de photo depuis l'app
|
||||||
|
- Recadrage et orientation automatique
|
||||||
|
- Envoi direct vers le back-end pour OCR
|
||||||
|
- Retour du texte extrait affiché à l'utilisateur
|
||||||
|
|
||||||
|
### Tagging
|
||||||
|
- Ajout de tags manuels sur chaque fichier
|
||||||
|
- Suggestion de tags basée sur le contenu OCR
|
||||||
|
- Filtrage par tag dans la liste
|
||||||
|
|
||||||
|
### Recherche
|
||||||
|
- Recherche full-text sur le contenu OCR
|
||||||
|
- Recherche par nom de fichier
|
||||||
|
- Filtres combinés (tag + texte)
|
||||||
|
|
||||||
|
### Listing
|
||||||
|
- Liste des fichiers uploadés avec aperçu
|
||||||
|
- Tri par date, nom, tag
|
||||||
|
- Pagination côté serveur
|
||||||
|
|
||||||
|
### Partage (Optionnel, Phase Later)
|
||||||
|
- Génération de lien temporaire
|
||||||
|
- Pas prioritaire en V1
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Exigences UX
|
||||||
|
|
||||||
|
- UI claire et épurée, minimaliste
|
||||||
|
- Temps de réponse < 2s pour les actions principales
|
||||||
|
- Feedback immédiat sur toutes les interactions
|
||||||
|
- Pas d'écran de chargement > 3s sans spinner
|
||||||
|
- Mode offline minimal : liste des fichiers déjà chargée visible même sans réseau
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Attentes API REST (Back-end Go)
|
||||||
|
|
||||||
|
### Endpoints
|
||||||
|
|
||||||
|
| Méthode | Path | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| GET | /api/v1/files | Liste des fichiers (pagination) |
|
||||||
|
| POST | /api/v1/files/upload | Upload d'un fichier |
|
||||||
|
| GET | /api/v1/files/:id | Détail d'un fichier |
|
||||||
|
| DELETE | /api/v1/files/:id | Suppression d'un fichier |
|
||||||
|
| GET | /api/v1/files/search?q= | Recherche full-text |
|
||||||
|
| POST | /api/v1/files/:id/tags | Ajout de tags |
|
||||||
|
| GET | /api/v1/files/:id/tags | Tags d'un fichier |
|
||||||
|
| POST | /api/v1/ocr/jobs | Soumettre un job OCR |
|
||||||
|
| GET | /api/v1/ocr/jobs/:id | Statut d'un job OCR |
|
||||||
|
| GET | /api/v1/health | Health check |
|
||||||
|
|
||||||
|
### Format de réponse standard
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": { ... },
|
||||||
|
"meta": {
|
||||||
|
"page": 1,
|
||||||
|
"total": 42
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Erreurs
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"code": "FILE_TOO_LARGE",
|
||||||
|
"message": "Fichier dépasse la limite de 50 Mo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Flux de Données
|
||||||
|
|
||||||
|
```
|
||||||
|
[Capture photo] ──> [Upload API] ──> [Back-end stocke]
|
||||||
|
│
|
||||||
|
v
|
||||||
|
[Job OCR créé]
|
||||||
|
│
|
||||||
|
v (async)
|
||||||
|
[Traitement OCR]
|
||||||
|
[Extraction texte]
|
||||||
|
[Indexation search]
|
||||||
|
│
|
||||||
|
v
|
||||||
|
[App affiche fichier] <──── [Polling statut] <── [Résultat prêt]
|
||||||
|
```
|
||||||
|
|
||||||
|
1. L'utilisateur prend une photo ou sélectionne un fichier
|
||||||
|
2. Le fichier est uploadé vers POST /api/v1/files/upload
|
||||||
|
3. Le back-end crée un job OCR et retourne immédiatement un ID
|
||||||
|
4. L'app poll GET /api/v1/ocr/jobs/:id ou utilise un webhook
|
||||||
|
5. Une fois l'OCR terminé, le texte est indexé et le fichier apparaît avec son contenu searchable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Choix Techniques
|
||||||
|
|
||||||
|
### Front-end (React Native)
|
||||||
|
- Expo managed workflow (développement rapide, build plus simple)
|
||||||
|
- TanStack Query (gestion serveur state, cache, refetch)
|
||||||
|
- React Navigation (navigation entre écrans)
|
||||||
|
- react-native-image-picker (sélection + caméra)
|
||||||
|
- MMKV (stockage clé-valeur pour cache local)
|
||||||
|
|
||||||
|
### Back-end (Go)
|
||||||
|
- Framework HTTP : Gin ou Fiber (au choix implémenteur)
|
||||||
|
- Persistance : SQLite pour V1 (volumétrie faible attendue)
|
||||||
|
- OCR : Tesseract en ligne de commande (appel système)
|
||||||
|
- Search : SQLite FTS5 pour la recherche full-text
|
||||||
|
- Stockage fichiers : disque local avec chemin référencé en base
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Structure de Dossiers Suggérée
|
||||||
|
|
||||||
|
### Front-end (React Native / Expo)
|
||||||
|
|
||||||
|
```
|
||||||
|
mobile/
|
||||||
|
├── app/ # Expo Router ou navigation
|
||||||
|
│ ├── index.tsx # Ecran principal (liste)
|
||||||
|
│ ├── upload.tsx # Ecran upload
|
||||||
|
│ ├── scan.tsx # Ecran scan caméra
|
||||||
|
│ └── search.tsx # Ecran recherche
|
||||||
|
├── components/
|
||||||
|
│ ├── FileCard.tsx
|
||||||
|
│ ├── TagChip.tsx
|
||||||
|
│ └── UploadProgress.tsx
|
||||||
|
├── hooks/
|
||||||
|
│ ├── useFiles.ts # TanStack Query hooks
|
||||||
|
│ ├── useSearch.ts
|
||||||
|
│ └── useUpload.ts
|
||||||
|
├── api/
|
||||||
|
│ └── client.ts # Client API (axios ou fetch)
|
||||||
|
├── types/
|
||||||
|
│ └── index.ts # Types TypeScript
|
||||||
|
├── constants/
|
||||||
|
│ └── api.ts # URLs, clés API
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Back-end (Go)
|
||||||
|
|
||||||
|
```
|
||||||
|
backend/
|
||||||
|
├── cmd/
|
||||||
|
│ └── server/
|
||||||
|
│ └── main.go # Point d'entrée
|
||||||
|
├── internal/
|
||||||
|
│ ├── handlers/ # Handlers HTTP
|
||||||
|
│ ├── models/ # Modèles de données
|
||||||
|
│ ├── repository/ # Accès données
|
||||||
|
│ ├── service/ # Logique métier
|
||||||
|
│ └── ocr/ # Module OCR
|
||||||
|
├── pkg/
|
||||||
|
│ └── api/
|
||||||
|
│ └── response.go # Helpers réponse
|
||||||
|
├── uploads/ # Fichiers stockés
|
||||||
|
├── go.mod
|
||||||
|
└── go.sum
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prérequis
|
||||||
|
|
||||||
|
- Node.js latest lts
|
||||||
|
- npm ou yarn
|
||||||
|
- Expo CLI (`npm install -g expo-cli`)
|
||||||
|
- Go latest lts
|
||||||
|
- Tesseract OCR installé (`apt install tesseract-ocr` sur Debian/Ubuntu)
|
||||||
|
|
||||||
|
### Installation (Front-end)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd mobile
|
||||||
|
npm install
|
||||||
|
npx expo start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Installation (Back-end)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
go mod download
|
||||||
|
go run cmd/server/main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build APK Android (Expo)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx expo run:android --variant release
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Variables d'Environnement
|
||||||
|
|
||||||
|
### Front-end (.env)
|
||||||
|
|
||||||
|
```
|
||||||
|
API_BASE_URL=http://localhost:8080/api/v1
|
||||||
|
TESSERACT_LANG=fr+eng
|
||||||
|
```
|
||||||
|
|
||||||
|
### Back-end (.env)
|
||||||
|
|
||||||
|
```
|
||||||
|
PORT=8080
|
||||||
|
UPLOAD_DIR=./uploads
|
||||||
|
MAX_FILE_SIZE_MB=50
|
||||||
|
OCR_LANG=fra+eng
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Itérations Futures
|
||||||
|
|
||||||
|
### V2 — Fiabilité et Performance
|
||||||
|
- Remplacement SQLite par PostgreSQL
|
||||||
|
- Upload chunked pour gros fichiers
|
||||||
|
- Compression d'images côté client
|
||||||
|
|
||||||
|
### V3 — Organisation Avancée
|
||||||
|
- Dossiers virtuels / hiérarchie
|
||||||
|
- Tags suggérés par IA
|
||||||
|
- OCR multilingue amélioré
|
||||||
|
|
||||||
|
### V4 — Collaboration
|
||||||
|
- Comptes utilisateurs
|
||||||
|
- Partage avec lien temporaire
|
||||||
|
- Rôle et permissions
|
||||||
|
|
||||||
|
### V5 — Plateforme
|
||||||
|
- Système de plugins (event bus)
|
||||||
|
- API publique
|
||||||
|
- Extensions tierces
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Statut du Projet
|
||||||
|
|
||||||
|
Phase : Conception et prototypage
|
||||||
|
Backend Go : En cours de structuration
|
||||||
|
Frontend React Native : À initier
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Binaries
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
vendor/
|
||||||
|
|
||||||
|
# Uploads
|
||||||
|
uploads/*
|
||||||
|
!uploads/.gitkeep
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/vaultdrop/backend/internal/handlers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
port := os.Getenv("PORT")
|
||||||
|
if port == "" {
|
||||||
|
port = "8080"
|
||||||
|
}
|
||||||
|
|
||||||
|
r := gin.Default()
|
||||||
|
|
||||||
|
r.GET("/api/v1/health", handlers.Health)
|
||||||
|
|
||||||
|
r.GET("/api/v1/files", handlers.ListFiles)
|
||||||
|
r.POST("/api/v1/files/upload", handlers.UploadFile)
|
||||||
|
r.GET("/api/v1/files/:id", handlers.GetFile)
|
||||||
|
r.DELETE("/api/v1/files/:id", handlers.DeleteFile)
|
||||||
|
r.GET("/api/v1/files/search", handlers.SearchFiles)
|
||||||
|
|
||||||
|
r.POST("/api/v1/files/:id/tags", handlers.AddTags)
|
||||||
|
r.GET("/api/v1/files/:id/tags", handlers.GetTags)
|
||||||
|
|
||||||
|
r.POST("/api/v1/ocr/jobs", handlers.CreateOcrJob)
|
||||||
|
r.GET("/api/v1/ocr/jobs/:id", handlers.GetOcrJobStatus)
|
||||||
|
|
||||||
|
log.Printf("Server starting on port %s", port)
|
||||||
|
if err := r.Run(":" + port); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
module github.com/vaultdrop/backend
|
||||||
|
|
||||||
|
go 1.26.4
|
||||||
|
|
||||||
|
require github.com/gin-gonic/gin v1.12.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
|
github.com/bytedance/sonic v1.15.0 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||||
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||||
|
golang.org/x/arch v0.22.0 // indirect
|
||||||
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
|
golang.org/x/net v0.51.0 // indirect
|
||||||
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
|
golang.org/x/text v0.34.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.10 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||||
|
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||||
|
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||||
|
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||||
|
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||||
|
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||||
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
|
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||||
|
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||||
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
|
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
|
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
|
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||||
|
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||||
|
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||||
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
|
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||||
|
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||||
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
|
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||||
|
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
|
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListFiles(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"data": []interface{}{},
|
||||||
|
"meta": gin.H{
|
||||||
|
"page": 1,
|
||||||
|
"total": 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFile(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{
|
||||||
|
"error": gin.H{
|
||||||
|
"code": "FILE_NOT_FOUND",
|
||||||
|
"message": "File not found",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func UploadFile(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusNotImplemented, gin.H{
|
||||||
|
"error": gin.H{
|
||||||
|
"code": "NOT_IMPLEMENTED",
|
||||||
|
"message": "Upload not yet implemented",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteFile(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusNotImplemented, gin.H{
|
||||||
|
"error": gin.H{
|
||||||
|
"code": "NOT_IMPLEMENTED",
|
||||||
|
"message": "Delete not yet implemented",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func SearchFiles(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"data": []interface{}{},
|
||||||
|
"meta": gin.H{
|
||||||
|
"page": 1,
|
||||||
|
"total": 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddTags(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusNotImplemented, gin.H{
|
||||||
|
"error": gin.H{
|
||||||
|
"code": "NOT_IMPLEMENTED",
|
||||||
|
"message": "Add tags not yet implemented",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTags(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"data": []interface{}{},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Health(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"data": gin.H{
|
||||||
|
"status": "healthy",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateOcrJob(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusNotImplemented, gin.H{
|
||||||
|
"error": gin.H{
|
||||||
|
"code": "NOT_IMPLEMENTED",
|
||||||
|
"message": "OCR job creation not yet implemented",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetOcrJobStatus(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{
|
||||||
|
"error": gin.H{
|
||||||
|
"code": "JOB_NOT_FOUND",
|
||||||
|
"message": "OCR job not found",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
MimeType string `json:"mimeType"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
Path string `json:"-"`
|
||||||
|
OcrText string `json:"ocrText,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tag struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OcrJob struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
FileID string `json:"fileId"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Result string `json:"result,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
CompletedAt *time.Time `json:"completedAt,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaginatedResponse struct {
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
Meta struct {
|
||||||
|
Page int `json:"page"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
} `json:"meta"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorResponse struct {
|
||||||
|
Error struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
} `json:"error"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Success(c *gin.Context, data interface{}) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"data": data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Created(c *gin.Context, data interface{}) {
|
||||||
|
c.JSON(http.StatusCreated, gin.H{
|
||||||
|
"data": data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Paginated(c *gin.Context, data interface{}, page, total int) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"data": data,
|
||||||
|
"meta": gin.H{
|
||||||
|
"page": page,
|
||||||
|
"total": total,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Error(c *gin.Context, status int, code, message string) {
|
||||||
|
c.JSON(status, gin.H{
|
||||||
|
"error": gin.H{
|
||||||
|
"code": code,
|
||||||
|
"message": message,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"enabledPlugins": {
|
||||||
|
"expo@claude-plugins-official": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Expo
|
||||||
|
.expo/
|
||||||
|
dist/
|
||||||
|
web-build/
|
||||||
|
expo-env.d.ts
|
||||||
|
|
||||||
|
# Native
|
||||||
|
.kotlin/
|
||||||
|
*.orig.*
|
||||||
|
*.jks
|
||||||
|
*.p8
|
||||||
|
*.p12
|
||||||
|
*.key
|
||||||
|
*.mobileprovision
|
||||||
|
|
||||||
|
# Metro
|
||||||
|
.metro-health-check*
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.*
|
||||||
|
yarn-debug.*
|
||||||
|
yarn-error.*
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# generated native folders
|
||||||
|
/ios
|
||||||
|
/android
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Expo HAS CHANGED
|
||||||
|
|
||||||
|
Read the exact versioned docs at https://docs.expo.dev/versions/v57.0.0/ before writing any code.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { StatusBar } from 'expo-status-bar';
|
||||||
|
import { NavigationContainer } from '@react-navigation/native';
|
||||||
|
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import { HomeScreen } from './app/index';
|
||||||
|
import { UploadScreen } from './app/upload';
|
||||||
|
import { ScanScreen } from './app/scan';
|
||||||
|
import { SearchScreen } from './app/search';
|
||||||
|
|
||||||
|
const Stack = createNativeStackNavigator();
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<NavigationContainer>
|
||||||
|
<Stack.Navigator initialRouteName="Home">
|
||||||
|
<Stack.Screen name="Home" component={HomeScreen} options={{ title: 'VaultDrop' }} />
|
||||||
|
<Stack.Screen name="Upload" component={UploadScreen} options={{ title: 'Upload' }} />
|
||||||
|
<Stack.Screen name="Scan" component={ScanScreen} options={{ title: 'Scan' }} />
|
||||||
|
<Stack.Screen name="Search" component={SearchScreen} options={{ title: 'Recherche' }} />
|
||||||
|
</Stack.Navigator>
|
||||||
|
</NavigationContainer>
|
||||||
|
<StatusBar style="auto" />
|
||||||
|
</QueryClientProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@AGENTS.md
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { API_BASE_URL } from '../constants/api';
|
||||||
|
import { ApiError } from '../types';
|
||||||
|
|
||||||
|
class ApiClient {
|
||||||
|
private baseUrl: string;
|
||||||
|
|
||||||
|
constructor(baseUrl: string) {
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async request<T>(
|
||||||
|
endpoint: string,
|
||||||
|
options: RequestInit = {}
|
||||||
|
): Promise<T> {
|
||||||
|
const url = `${this.baseUrl}${endpoint}`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error: ApiError = await response.json();
|
||||||
|
throw new Error(error.error?.message || 'Request failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async get<T>(endpoint: string): Promise<T> {
|
||||||
|
return this.request<T>(endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
async post<T>(endpoint: string, body?: unknown): Promise<T> {
|
||||||
|
return this.request<T>(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete<T>(endpoint: string): Promise<T> {
|
||||||
|
return this.request<T>(endpoint, { method: 'DELETE' });
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadFile<T>(endpoint: string, formData: FormData): Promise<T> {
|
||||||
|
const url = `${this.baseUrl}${endpoint}`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error: ApiError = await response.json();
|
||||||
|
throw new Error(error.error?.message || 'Upload failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const apiClient = new ApiClient(API_BASE_URL);
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"expo": {
|
||||||
|
"name": "webui",
|
||||||
|
"slug": "webui",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"orientation": "portrait",
|
||||||
|
"icon": "./assets/icon.png",
|
||||||
|
"userInterfaceStyle": "light",
|
||||||
|
"ios": {
|
||||||
|
"supportsTablet": true
|
||||||
|
},
|
||||||
|
"android": {
|
||||||
|
"adaptiveIcon": {
|
||||||
|
"backgroundColor": "#E6F4FE",
|
||||||
|
"foregroundImage": "./assets/android-icon-foreground.png",
|
||||||
|
"backgroundImage": "./assets/android-icon-background.png",
|
||||||
|
"monochromeImage": "./assets/android-icon-monochrome.png"
|
||||||
|
},
|
||||||
|
"predictiveBackGestureEnabled": false
|
||||||
|
},
|
||||||
|
"web": {
|
||||||
|
"favicon": "./assets/favicon.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { View, FlatList, StyleSheet, TouchableOpacity, Text } from 'react-native';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||||
|
import { useFiles } from '../hooks/useFiles';
|
||||||
|
import { FileCard } from '../components/FileCard';
|
||||||
|
import { FileItem } from '../types';
|
||||||
|
|
||||||
|
type RootStackParamList = {
|
||||||
|
Home: undefined;
|
||||||
|
Upload: undefined;
|
||||||
|
Scan: undefined;
|
||||||
|
Search: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
|
||||||
|
|
||||||
|
export function HomeScreen() {
|
||||||
|
const navigation = useNavigation<NavigationProp>();
|
||||||
|
const { data, isLoading, error } = useFiles();
|
||||||
|
|
||||||
|
const renderItem = ({ item }: { item: FileItem }) => (
|
||||||
|
<FileCard
|
||||||
|
file={item}
|
||||||
|
onPress={(file) => console.log('File pressed:', file.id)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<View style={styles.center}>
|
||||||
|
<Text>Chargement...</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<View style={styles.center}>
|
||||||
|
<Text>Erreur de chargement</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<FlatList
|
||||||
|
data={data?.data || []}
|
||||||
|
renderItem={renderItem}
|
||||||
|
keyExtractor={(item) => item.id}
|
||||||
|
contentContainerStyle={styles.list}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<View style={styles.bottomNav}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.navButton}
|
||||||
|
onPress={() => navigation.navigate('Upload')}
|
||||||
|
>
|
||||||
|
<Text style={styles.navText}>Upload</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.navButton}
|
||||||
|
onPress={() => navigation.navigate('Scan')}
|
||||||
|
>
|
||||||
|
<Text style={styles.navText}>Scan</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.navButton}
|
||||||
|
onPress={() => navigation.navigate('Search')}
|
||||||
|
>
|
||||||
|
<Text style={styles.navText}>Recherche</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#f5f5f5',
|
||||||
|
},
|
||||||
|
center: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
bottomNav: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-around',
|
||||||
|
paddingVertical: 12,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: '#e0e0e0',
|
||||||
|
},
|
||||||
|
navButton: {
|
||||||
|
padding: 8,
|
||||||
|
},
|
||||||
|
navText: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: '#1976D2',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { View, Text, StyleSheet, TouchableOpacity, Alert } from 'react-native';
|
||||||
|
import * as ImagePicker from 'react-native-image-picker';
|
||||||
|
|
||||||
|
export function ScanScreen() {
|
||||||
|
const takePhoto = async () => {
|
||||||
|
try {
|
||||||
|
const result = await ImagePicker.launchCamera({
|
||||||
|
mediaType: 'photo',
|
||||||
|
quality: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.didCancel) return;
|
||||||
|
if (result.errorCode) {
|
||||||
|
Alert.alert('Erreur', result.errorMessage || "Impossible d'accéder à la caméra");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.assets && result.assets[0]) {
|
||||||
|
console.log('Photo taken:', result.assets[0]);
|
||||||
|
// TODO: Send to backend for OCR
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Alert.alert('Erreur', "Impossible d'accéder à la caméra");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text style={styles.title}>Scanner un document</Text>
|
||||||
|
<Text style={styles.description}>
|
||||||
|
Prenez une photo de votre document pour extraire le texte via OCR
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<TouchableOpacity style={styles.scanButton} onPress={takePhoto}>
|
||||||
|
<Text style={styles.scanText}>Prendre une photo</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
padding: 16,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: '700',
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: '#666',
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: 32,
|
||||||
|
},
|
||||||
|
scanButton: {
|
||||||
|
backgroundColor: '#4CAF50',
|
||||||
|
padding: 16,
|
||||||
|
borderRadius: 8,
|
||||||
|
width: '100%',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
scanText: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { View, TextInput, FlatList, StyleSheet, Text } from 'react-native';
|
||||||
|
import { useSearch } from '../hooks/useSearch';
|
||||||
|
import { FileCard } from '../components/FileCard';
|
||||||
|
import { FileItem } from '../types';
|
||||||
|
|
||||||
|
export function SearchScreen() {
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
const { data, isLoading } = useSearch(query);
|
||||||
|
|
||||||
|
const renderItem = ({ item }: { item: FileItem }) => (
|
||||||
|
<FileCard
|
||||||
|
file={item}
|
||||||
|
onPress={(file) => console.log('File pressed:', file.id)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
placeholder="Rechercher un fichier..."
|
||||||
|
value={query}
|
||||||
|
onChangeText={setQuery}
|
||||||
|
autoCapitalize="none"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isLoading && <Text style={styles.loading}>Recherche en cours...</Text>}
|
||||||
|
|
||||||
|
<FlatList
|
||||||
|
data={data?.data || []}
|
||||||
|
renderItem={renderItem}
|
||||||
|
keyExtractor={(item) => item.id}
|
||||||
|
contentContainerStyle={styles.list}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#f5f5f5',
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
padding: 12,
|
||||||
|
margin: 16,
|
||||||
|
borderRadius: 8,
|
||||||
|
fontSize: 16,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#e0e0e0',
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
textAlign: 'center',
|
||||||
|
color: '#666',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { View, Text, StyleSheet, TouchableOpacity, Alert } from 'react-native';
|
||||||
|
import * as ImagePicker from 'react-native-image-picker';
|
||||||
|
import { useUpload } from '../hooks/useUpload';
|
||||||
|
import { UploadProgress } from '../components/UploadProgress';
|
||||||
|
|
||||||
|
export function UploadScreen() {
|
||||||
|
const [uploadStatus, setUploadStatus] = useState<'idle' | 'uploading' | 'processing' | 'success' | 'error'>('idle');
|
||||||
|
const [error, setError] = useState<string>();
|
||||||
|
const upload = useUpload();
|
||||||
|
|
||||||
|
const pickImage = async () => {
|
||||||
|
try {
|
||||||
|
const result = await ImagePicker.launchImageLibrary({
|
||||||
|
mediaType: 'photo',
|
||||||
|
quality: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.didCancel) return;
|
||||||
|
if (result.errorCode) {
|
||||||
|
Alert.alert('Erreur', result.errorMessage || "Impossible d'accéder à la galerie");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.assets && result.assets[0]) {
|
||||||
|
setUploadStatus('uploading');
|
||||||
|
const asset = result.assets[0];
|
||||||
|
const file = {
|
||||||
|
uri: asset.uri,
|
||||||
|
type: asset.type || 'image/jpeg',
|
||||||
|
name: asset.fileName || 'photo.jpg',
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await upload.mutateAsync(file as any);
|
||||||
|
setUploadStatus('processing');
|
||||||
|
console.log('Upload success:', response);
|
||||||
|
setUploadStatus('success');
|
||||||
|
} catch (err) {
|
||||||
|
setUploadStatus('error');
|
||||||
|
setError(err instanceof Error ? err.message : 'Upload failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Alert.alert('Erreur', "Impossible d'accéder à la galerie");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<UploadProgress status={uploadStatus} error={error} />
|
||||||
|
|
||||||
|
<TouchableOpacity style={styles.uploadButton} onPress={pickImage}>
|
||||||
|
<Text style={styles.uploadText}>Sélectionner une photo</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
padding: 16,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
},
|
||||||
|
uploadButton: {
|
||||||
|
backgroundColor: '#1976D2',
|
||||||
|
padding: 16,
|
||||||
|
borderRadius: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
uploadText: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
});
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 384 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,81 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
|
||||||
|
import { FileItem } from '../types';
|
||||||
|
import { TagChip } from './TagChip';
|
||||||
|
|
||||||
|
interface FileCardProps {
|
||||||
|
file: FileItem;
|
||||||
|
onPress?: (file: FileItem) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FileCard({ file, onPress }: FileCardProps) {
|
||||||
|
const formatSize = (bytes: number) => {
|
||||||
|
if (bytes < 1024) return `${bytes} B`;
|
||||||
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||||
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity style={styles.container} onPress={() => onPress?.(file)}>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<Text style={styles.name} numberOfLines={1}>
|
||||||
|
{file.name}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.size}>{formatSize(file.size)}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{file.ocrText && (
|
||||||
|
<Text style={styles.preview} numberOfLines={2}>
|
||||||
|
{file.ocrText}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View style={styles.tags}>
|
||||||
|
{file.tags.map((tag) => (
|
||||||
|
<TagChip key={tag.id} name={tag.name} />
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 16,
|
||||||
|
marginBottom: 12,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 1 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 2,
|
||||||
|
elevation: 2,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
flex: 1,
|
||||||
|
marginRight: 8,
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#666',
|
||||||
|
},
|
||||||
|
preview: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#444',
|
||||||
|
marginBottom: 8,
|
||||||
|
lineHeight: 20,
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 6,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
|
||||||
|
|
||||||
|
interface TagChipProps {
|
||||||
|
name: string;
|
||||||
|
onRemove?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TagChip({ name, onRemove }: TagChipProps) {
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text style={styles.text}>{name}</Text>
|
||||||
|
{onRemove && (
|
||||||
|
<TouchableOpacity onPress={onRemove} style={styles.removeButton}>
|
||||||
|
<Text style={styles.removeText}>×</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
backgroundColor: '#E3F2FD',
|
||||||
|
borderRadius: 12,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
paddingVertical: 4,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#1976D2',
|
||||||
|
},
|
||||||
|
removeButton: {
|
||||||
|
marginLeft: 4,
|
||||||
|
},
|
||||||
|
removeText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#1976D2',
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { View, Text, StyleSheet, ActivityIndicator } from 'react-native';
|
||||||
|
|
||||||
|
interface UploadProgressProps {
|
||||||
|
progress?: number;
|
||||||
|
status: 'idle' | 'uploading' | 'processing' | 'success' | 'error';
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UploadProgress({ progress, status, error }: UploadProgressProps) {
|
||||||
|
if (status === 'idle') return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
{status === 'uploading' && (
|
||||||
|
<>
|
||||||
|
<ActivityIndicator size="small" color="#1976D2" />
|
||||||
|
<Text style={styles.text}>
|
||||||
|
Upload en cours... {progress !== undefined && `${progress}%`}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status === 'processing' && (
|
||||||
|
<>
|
||||||
|
<ActivityIndicator size="small" color="#1976D2" />
|
||||||
|
<Text style={styles.text}>Traitement OCR en cours...</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status === 'success' && (
|
||||||
|
<Text style={[styles.text, styles.success]}>Upload terminé !</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status === 'error' && (
|
||||||
|
<Text style={[styles.text, styles.error]}>
|
||||||
|
{error || "Erreur lors de l'upload"}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 12,
|
||||||
|
backgroundColor: '#F5F5F5',
|
||||||
|
borderRadius: 8,
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
marginLeft: 8,
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#333',
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
color: '#4CAF50',
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
color: '#F44336',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export const API_BASE_URL = process.env.EXPO_PUBLIC_API_BASE_URL || 'http://localhost:8080/api/v1';
|
||||||
|
|
||||||
|
export const ENDPOINTS = {
|
||||||
|
FILES: '/files',
|
||||||
|
UPLOAD: '/files/upload',
|
||||||
|
SEARCH: '/files/search',
|
||||||
|
OCR_JOBS: '/ocr/jobs',
|
||||||
|
HEALTH: '/health',
|
||||||
|
} as const;
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { apiClient } from '../api/client';
|
||||||
|
import { ENDPOINTS } from '../constants/api';
|
||||||
|
import { FileItem, PaginatedResponse } from '../types';
|
||||||
|
|
||||||
|
export function useFiles(page: number = 1, limit: number = 20) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['files', page, limit],
|
||||||
|
queryFn: () =>
|
||||||
|
apiClient.get<PaginatedResponse<FileItem>>(
|
||||||
|
`${ENDPOINTS.FILES}?page=${page}&limit=${limit}`
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFile(id: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['files', id],
|
||||||
|
queryFn: () => apiClient.get<FileItem>(`${ENDPOINTS.FILES}/${id}`),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteFile() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: string) => apiClient.delete(`${ENDPOINTS.FILES}/${id}`),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['files'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { apiClient } from '../api/client';
|
||||||
|
import { ENDPOINTS } from '../constants/api';
|
||||||
|
import { FileItem, PaginatedResponse } from '../types';
|
||||||
|
|
||||||
|
export function useSearch(query: string, page: number = 1, limit: number = 20) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['search', query, page, limit],
|
||||||
|
queryFn: () =>
|
||||||
|
apiClient.get<PaginatedResponse<FileItem>>(
|
||||||
|
`${ENDPOINTS.FILES}/search?q=${encodeURIComponent(query)}&page=${page}&limit=${limit}`
|
||||||
|
),
|
||||||
|
enabled: query.length > 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { apiClient } from '../api/client';
|
||||||
|
import { ENDPOINTS } from '../constants/api';
|
||||||
|
import { OcrJob } from '../types';
|
||||||
|
|
||||||
|
export function useUpload() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (file: File) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
return apiClient.uploadFile<{ id: string; ocrJobId: string }>(
|
||||||
|
ENDPOINTS.UPLOAD,
|
||||||
|
formData
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['files'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useOcrJobStatus(jobId: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['ocr', jobId],
|
||||||
|
queryFn: () => apiClient.get<OcrJob>(`${ENDPOINTS.OCR_JOBS}/${jobId}`),
|
||||||
|
enabled: !!jobId,
|
||||||
|
refetchInterval: (query: any) => {
|
||||||
|
const status = query.state.data?.status;
|
||||||
|
if (status === 'completed' || status === 'failed') return false;
|
||||||
|
return 1000;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { registerRootComponent } from 'expo';
|
||||||
|
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
|
||||||
|
// It also ensures that whether you load the app in Expo Go or in a native build,
|
||||||
|
// the environment is set up appropriately
|
||||||
|
registerRootComponent(App);
|
||||||
Generated
+6316
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "webui",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.ts",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-navigation/native": "^7.3.8",
|
||||||
|
"@react-navigation/native-stack": "^7.17.10",
|
||||||
|
"@tanstack/react-query": "^5.101.2",
|
||||||
|
"expo": "~57.0.4",
|
||||||
|
"expo-status-bar": "~57.0.0",
|
||||||
|
"react": "19.2.3",
|
||||||
|
"react-native": "0.86.0",
|
||||||
|
"react-native-image-picker": "^8.2.1",
|
||||||
|
"react-native-mmkv": "^4.3.2",
|
||||||
|
"react-native-safe-area-context": "~5.7.0",
|
||||||
|
"react-native-screens": "4.25.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "~19.2.2",
|
||||||
|
"typescript": "~6.0.3"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "expo start",
|
||||||
|
"android": "expo start --android",
|
||||||
|
"ios": "expo start --ios",
|
||||||
|
"web": "expo start --web"
|
||||||
|
},
|
||||||
|
"private": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": "expo/tsconfig.base",
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
export interface FileItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
mimeType: string;
|
||||||
|
size: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
ocrText?: string;
|
||||||
|
tags: Tag[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Tag {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OcrJob {
|
||||||
|
id: string;
|
||||||
|
fileId: string;
|
||||||
|
status: 'pending' | 'processing' | 'completed' | 'failed';
|
||||||
|
result?: string;
|
||||||
|
createdAt: string;
|
||||||
|
completedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginatedResponse<T> {
|
||||||
|
data: T[];
|
||||||
|
meta: {
|
||||||
|
page: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiError {
|
||||||
|
error: {
|
||||||
|
code: string;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user