fix(audio+gps): lecture audio multi-navigateur + icône GPS dans tuile note

Audio :
- MediaRecorder détecte le format supporté : webm (Chrome/Firefox) ou mp4 (Safari/iOS)
- Extension sauvegardée correctement (.webm ou .m4a) selon le navigateur
- Backend : ALLOWED_AUDIO_PREFIXES remplace le set strict, strip des codec suffixes

GPS (note card) :
- Icône fa-location-dot (accent vert) avec tooltip lat/lon remplace l'emoji 📍

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 15:55:35 +02:00
co-authored by Claude Sonnet 4.6
parent 9de8ad5f3e
commit 9aaa5fb562
3 changed files with 16 additions and 10 deletions
+4 -3
View File
@@ -9,7 +9,7 @@ from app.core.database import get_session
from app.core.redis import enqueue from app.core.redis import enqueue
from app.models.notes import NoteItem, NoteAttachment from app.models.notes import NoteItem, NoteAttachment
from app.schemas.notes import NoteCreate, NoteUpdate, NoteResponse from app.schemas.notes import NoteCreate, NoteUpdate, NoteResponse
from app.services.media import save_image, save_audio, delete_media, ALLOWED_IMAGE_TYPES, ALLOWED_AUDIO_TYPES from app.services.media import save_image, save_audio, delete_media, ALLOWED_IMAGE_TYPES, ALLOWED_AUDIO_PREFIXES
router = APIRouter() router = APIRouter()
@@ -130,10 +130,11 @@ async def add_attachment(
if not note: if not note:
raise HTTPException(404, "Note introuvable") raise HTTPException(404, "Note introuvable")
if file.content_type in ALLOWED_IMAGE_TYPES: ct = (file.content_type or "").lower().split(";")[0].strip()
if ct in ALLOWED_IMAGE_TYPES:
media = await save_image(file, context="note") media = await save_image(file, context="note")
file_type = "image" file_type = "image"
elif file.content_type in ALLOWED_AUDIO_TYPES: elif ct in ALLOWED_AUDIO_PREFIXES:
media = await save_audio(file) media = await save_audio(file)
file_type = "audio" file_type = "audio"
else: else:
+3 -2
View File
@@ -13,7 +13,7 @@ ALLOWED_IMAGE_TYPES = {
"image/jpeg", "image/jpg", "image/png", "image/webp", "image/svg+xml", "image/jpeg", "image/jpg", "image/png", "image/webp", "image/svg+xml",
"image/heic", "image/heif", "image/heic", "image/heif",
} }
ALLOWED_AUDIO_TYPES = {"audio/webm", "audio/mp4"} ALLOWED_AUDIO_PREFIXES = {"audio/webm", "audio/mp4", "audio/ogg", "audio/x-m4a"}
MAX_ORIG_SIZE = (500, 500) MAX_ORIG_SIZE = (500, 500)
@@ -70,7 +70,8 @@ async def save_image(file: UploadFile, context: str = "note") -> dict:
async def save_audio(file: UploadFile) -> dict: async def save_audio(file: UploadFile) -> dict:
if file.content_type not in ALLOWED_AUDIO_TYPES: ct = (file.content_type or "").lower().split(";")[0].strip()
if ct not in ALLOWED_AUDIO_PREFIXES:
raise HTTPException(status_code=400, detail=f"Format audio non supporté : {file.content_type}") raise HTTPException(status_code=400, detail=f"Format audio non supporté : {file.content_type}")
file_id = str(uuid.uuid4()) file_id = str(uuid.uuid4())
+9 -5
View File
@@ -41,19 +41,23 @@ function NoteCard({ note, onEdit, onDelete, onAddPhoto, onAddAudio, onDeleteAtt
async function startRecord() { async function startRecord() {
try { try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
const recorder = new MediaRecorder(stream) // Choisir le format supporté par le navigateur (Safari → mp4, Chrome/Firefox → webm)
const mimeType = ['audio/webm', 'audio/mp4', 'audio/ogg'].find(t => MediaRecorder.isTypeSupported(t)) ?? ''
const recorder = mimeType ? new MediaRecorder(stream, { mimeType }) : new MediaRecorder(stream)
chunksRef.current = [] chunksRef.current = []
recorder.ondataavailable = e => { if (e.data.size > 0) chunksRef.current.push(e.data) } recorder.ondataavailable = e => { if (e.data.size > 0) chunksRef.current.push(e.data) }
recorder.onstop = () => { recorder.onstop = () => {
stream.getTracks().forEach(t => t.stop()) stream.getTracks().forEach(t => t.stop())
const blob = new Blob(chunksRef.current, { type: 'audio/webm' }) const type = recorder.mimeType || mimeType || 'audio/webm'
onAddAudio(new File([blob], 'enregistrement.webm', { type: 'audio/webm' })) const ext = type.includes('mp4') ? 'm4a' : 'webm'
const blob = new Blob(chunksRef.current, { type })
onAddAudio(new File([blob], `enregistrement.${ext}`, { type }))
} }
recorder.start() recorder.start()
recorderRef.current = recorder recorderRef.current = recorder
setRecording(true) setRecording(true)
} catch { } catch {
// micro non disponible // micro non disponible ou permission refusée
} }
} }
@@ -121,7 +125,7 @@ function NoteCard({ note, onEdit, onDelete, onAddPhoto, onAddAudio, onDeleteAtt
{note.tags.map(t => ( {note.tags.map(t => (
<span key={t} style={{ background: 'var(--bg-5)', color: 'var(--ink-3)', fontSize: 11, fontFamily: 'var(--font-ui)', borderRadius: 999, padding: '1px 7px' }}>{t}</span> <span key={t} style={{ background: 'var(--bg-5)', color: 'var(--ink-3)', fontSize: 11, fontFamily: 'var(--font-ui)', borderRadius: 999, padding: '1px 7px' }}>{t}</span>
))} ))}
{note.gps_lat && <span style={{ color: 'var(--ok)', fontSize: 11 }}>📍</span>} {note.gps_lat != null && <i className="fa-solid fa-location-dot" style={{ color: 'var(--ok)', fontSize: 12 }} title={`${note.gps_lat?.toFixed(4)}, ${note.gps_lon?.toFixed(4)}`} />}
</div> </div>
{/* Actions */} {/* Actions */}