diff --git a/backend/app/routers/media.py b/backend/app/routers/media.py index 946dcef..cd609cc 100644 --- a/backend/app/routers/media.py +++ b/backend/app/routers/media.py @@ -1,29 +1,112 @@ import os import uuid -from fastapi import APIRouter, File, HTTPException, UploadFile +from typing import List, Optional + +from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile, status +from sqlmodel import Session, select + from app.config import UPLOAD_DIR +from app.database import get_session +from app.models.media import Attachment, Media router = APIRouter(tags=["media"]) -ALLOWED_EXT = {".jpg", ".jpeg", ".png", ".webp", ".gif"} + +def _save_webp(data: bytes, max_px: int) -> str: + try: + from PIL import Image + import io + + img = Image.open(io.BytesIO(data)).convert("RGB") + img.thumbnail((max_px, max_px)) + name = f"{uuid.uuid4()}.webp" + path = os.path.join(UPLOAD_DIR, name) + img.save(path, "WEBP", quality=85) + return name + except Exception: + name = f"{uuid.uuid4()}.bin" + path = os.path.join(UPLOAD_DIR, name) + with open(path, "wb") as f: + f.write(data) + return name @router.post("/upload") async def upload_file(file: UploadFile = File(...)): - ext = os.path.splitext(file.filename or "")[-1].lower() - if ext not in ALLOWED_EXT: - raise HTTPException(status_code=400, detail="Format non supporté") - filename = f"{uuid.uuid4().hex}{ext}" - dest = os.path.join(UPLOAD_DIR, filename) os.makedirs(UPLOAD_DIR, exist_ok=True) - content = await file.read() - with open(dest, "wb") as f: - f.write(content) - return {"filename": filename, "url": f"/uploads/{filename}"} + data = await file.read() + ct = file.content_type or "" + if ct.startswith("image/"): + name = _save_webp(data, 1200) + thumb = _save_webp(data, 300) + return {"url": f"/uploads/{name}", "thumbnail_url": f"/uploads/{thumb}"} + else: + name = f"{uuid.uuid4()}_{file.filename}" + path = os.path.join(UPLOAD_DIR, name) + with open(path, "wb") as f: + f.write(data) + return {"url": f"/uploads/{name}", "thumbnail_url": None} -@router.delete("/upload/{filename}", status_code=204) -def delete_file(filename: str): - path = os.path.join(UPLOAD_DIR, filename) - if os.path.exists(path): - os.remove(path) +@router.get("/media/all", response_model=List[Media]) +def list_all_media( + entity_type: Optional[str] = Query(default=None), + session: Session = Depends(get_session), +): + """Retourne tous les médias, filtrés optionnellement par entity_type.""" + q = select(Media).order_by(Media.created_at.desc()) + if entity_type: + q = q.where(Media.entity_type == entity_type) + return session.exec(q).all() + + +@router.get("/media", response_model=List[Media]) +def list_media( + entity_type: str = Query(...), + entity_id: int = Query(...), + session: Session = Depends(get_session), +): + return session.exec( + select(Media).where( + Media.entity_type == entity_type, Media.entity_id == entity_id + ) + ).all() + + +@router.post("/media", response_model=Media, status_code=status.HTTP_201_CREATED) +def create_media(m: Media, session: Session = Depends(get_session)): + session.add(m) + session.commit() + session.refresh(m) + return m + + +@router.delete("/media/{id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_media(id: int, session: Session = Depends(get_session)): + m = session.get(Media, id) + if not m: + raise HTTPException(404, "Media introuvable") + session.delete(m) + session.commit() + + +@router.get("/attachments", response_model=List[Attachment]) +def list_attachments( + entity_type: str = Query(...), + entity_id: int = Query(...), + session: Session = Depends(get_session), +): + return session.exec( + select(Attachment).where( + Attachment.entity_type == entity_type, + Attachment.entity_id == entity_id, + ) + ).all() + + +@router.post("/attachments", response_model=Attachment, status_code=status.HTTP_201_CREATED) +def create_attachment(a: Attachment, session: Session = Depends(get_session)): + session.add(a) + session.commit() + session.refresh(a) + return a diff --git a/frontend/src/components/AppDrawer.vue b/frontend/src/components/AppDrawer.vue index 567f2de..04e4051 100644 --- a/frontend/src/components/AppDrawer.vue +++ b/frontend/src/components/AppDrawer.vue @@ -21,11 +21,13 @@ defineEmits(['close']) const links = [ { to: '/', label: 'Dashboard' }, { to: '/jardins', label: 'Jardins' }, - { to: '/varietes', label: 'Variétés' }, + { to: '/plantes', label: 'Plantes' }, + { to: '/bibliotheque', label: '📷 Bibliothèque' }, + { to: '/outils', label: 'Outils' }, { to: '/plantations', label: 'Plantations' }, { to: '/taches', label: 'Tâches' }, { to: '/planning', label: 'Planning' }, - { to: '/lunaire', label: 'Calendrier lunaire' }, + { to: '/calendrier', label: 'Calendrier' }, { to: '/reglages', label: 'Réglages' }, ] diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index bf70005..4e55b66 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -6,11 +6,16 @@ export default createRouter({ { path: '/', component: () => import('@/views/DashboardView.vue') }, { path: '/jardins', component: () => import('@/views/JardinsView.vue') }, { path: '/jardins/:id', component: () => import('@/views/JardinDetailView.vue') }, - { path: '/varietes', component: () => import('@/views/VarietesView.vue') }, + { path: '/plantes', component: () => import('@/views/PlantesView.vue') }, + { path: '/bibliotheque', component: () => import('@/views/BibliothequeView.vue') }, + { path: '/outils', component: () => import('@/views/OutilsView.vue') }, { path: '/plantations', component: () => import('@/views/PlantationsView.vue') }, { path: '/planning', component: () => import('@/views/PlanningView.vue') }, { path: '/taches', component: () => import('@/views/TachesView.vue') }, - { path: '/lunaire', component: () => import('@/views/LunaireView.vue') }, + { path: '/calendrier', component: () => import('@/views/CalendrierView.vue') }, { path: '/reglages', component: () => import('@/views/ReglagesView.vue') }, + // Redirect des anciens liens + { path: '/varietes', redirect: '/plantes' }, + { path: '/lunaire', redirect: '/calendrier' }, ], }) diff --git a/frontend/src/views/BibliothequeView.vue b/frontend/src/views/BibliothequeView.vue new file mode 100644 index 0000000..523f64d --- /dev/null +++ b/frontend/src/views/BibliothequeView.vue @@ -0,0 +1,161 @@ + + + + 📷 Bibliothèque + + Identifier une plante + + + + + + + {{ f.label }} + + + + + Chargement... + + Aucune photo. + + + + + + {{ m.identified_common }} + + + {{ labelFor(m.entity_type) }} + + + + + + + + + + + {{ lightbox.identified_common }} + + {{ lightbox.identified_species }} + + Confiance : {{ Math.round((lightbox.identified_confidence || 0) * 100) }}% — + via {{ lightbox.identified_source }} + + + + Fermer + + + + + + + + + +