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:
@@ -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:
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
Reference in New Issue
Block a user