Files
parenthese/frontend/src/hooks/useMediaManager.js
T
IliesAllaliandClaude Opus 5.5 f65273e95a Personnes identifiées sur un souvenir
Sur une photo ou une vidéo, on ajoute les personnes de l'arbre qui y figurent et, sur une photo,
on touche leur visage pour poser un point. Le souvenir apparaît aussi sur la fiche de chaque
personne identifiée (« Avec Odette »), un prénom ouvre sa fiche et recentre l'arbre.

L'administrateur enregistre directement. La famille propose, l'administrateur relit dans
« Propositions à relire ». Suppression d'une personne ou d'un souvenir : identifications retirées.

Table media_person_tags (migration additive), route PUT /trees/:id/media/:mediaId/people.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 20:32:22 +02:00

327 lines
10 KiB
JavaScript

import { useState } from 'react'
import { uploadPersonMedia, reorderPersonMedia, deletePersonMedia, updatePersonMedia, updateMediaPeople, fetchPersonLife } from '../api/treeApi'
import { getPersonMedias } from '../data/mockData'
import { fileToBase64, getAccountErrorMessage } from '../utils/errorMessages'
import { getUploadMimeType, resolveUploadMediaType, validateMediaFile } from '../utils/mediaUpload'
import { readContributorName, saveContributorName } from '../utils/contributorName'
import { t } from '../i18n/index.js'
// Souvenir envoyé par la famille : en attente de la relecture du propriétaire
function noticeFor(created) {
return created?.status === 'pending'
? t("Merci, c'est envoyé. Le souvenir apparaîtra dans la fiche quand la personne qui gère l'arbre l'aura accepté.")
: ''
}
export function useMediaManager({
canEditCurrentTree,
canSubmitContribution,
selectedPerson,
treeContext,
refreshCurrentTreeGraph,
refreshTreeAndKeepSelection,
}) {
const [mediaManagerState, setMediaManagerState] = useState({ visible: false, loading: false, error: '', notice: '' })
const selectedPersonMedias = selectedPerson ? getPersonMedias(selectedPerson.id) : []
const canUploadMedia = canEditCurrentTree || canSubmitContribution
const handleOpenMediaManager = () => {
if (!canUploadMedia || !selectedPerson) {
return
}
setMediaManagerState({ visible: true, loading: false, error: '', notice: '' })
}
const handleCloseMediaManager = () => {
setMediaManagerState((current) => ({
...current,
visible: false,
error: '',
}))
}
const refreshAfterMediaMutation = async (personId) => {
if (typeof refreshTreeAndKeepSelection === 'function') {
await refreshTreeAndKeepSelection(personId, { relayout: false })
return
}
await refreshCurrentTreeGraph({ relayout: false })
}
const handleUploadPersonMedia = async ({
file,
caption,
source = null,
mediaType,
citationText = '',
youtubeUrl = null,
submittedByLabel = null,
}) => {
if (!canUploadMedia || !selectedPerson || !treeContext.treeId || !treeContext.accessToken) {
return false
}
// Prénom de la personne qui propose (famille), retenu pour la fois suivante
const byLabel = submittedByLabel ? { submittedByLabel } : {}
if (submittedByLabel) saveContributorName(submittedByLabel)
const personId = selectedPerson.id
// YouTube video — no file needed
if (mediaType === 'video' && youtubeUrl) {
setMediaManagerState((current) => ({ ...current, loading: true, error: '', notice: '' }))
try {
const created = await uploadPersonMedia(
treeContext.treeId,
personId,
{
type: 'video',
mimeType: 'video/youtube',
youtubeUrl,
caption: caption || null,
source,
...byLabel,
},
treeContext.accessToken,
)
await refreshAfterMediaMutation(personId)
setMediaManagerState((current) => ({ ...current, loading: false, error: '', notice: noticeFor(created) }))
} catch (error) {
setMediaManagerState((current) => ({ ...current, loading: false, error: getAccountErrorMessage(error) }))
}
return
}
const resolvedType = resolveUploadMediaType(file, mediaType)
const resolvedCitationText = String(citationText || caption || '').trim()
const validationError = validateMediaFile(file, resolvedType, { citationText: resolvedCitationText })
if (validationError) {
setMediaManagerState((current) => ({
...current,
loading: false,
error: validationError,
}))
return false
}
setMediaManagerState((current) => ({
...current,
loading: true,
error: '',
notice: '',
}))
try {
let created = null
if (resolvedType === 'citation') {
created = await uploadPersonMedia(
treeContext.treeId,
personId,
{
type: 'citation',
caption: resolvedCitationText,
source,
...byLabel,
},
treeContext.accessToken,
)
} else {
const dataBase64 = await fileToBase64(file)
created = await uploadPersonMedia(
treeContext.treeId,
personId,
{
type: resolvedType,
fileName: file.name || 'upload.jpg',
mimeType: getUploadMimeType(file, resolvedType),
sizeBytes: file.size,
dataBase64,
caption,
source,
...byLabel,
},
treeContext.accessToken,
)
}
await refreshAfterMediaMutation(personId)
setMediaManagerState((current) => ({
...current,
loading: false,
error: '',
notice: noticeFor(created),
}))
return true
} catch (error) {
setMediaManagerState((current) => ({
...current,
loading: false,
error: getAccountErrorMessage(error),
}))
return false
}
}
// Fichier d'un moment de l'onglet Vie (photo, vidéo, enregistrement, acte en PDF…) : envoyé comme un
// souvenir de la personne et rattaché au moment. Pour la famille, il rejoint la relecture du moment.
// Renvoie le souvenir créé ; une erreur remonte avec un message lisible.
const uploadMomentFile = async (personId, file, { lifeEventId, caption = null, submittedByLabel = null } = {}) => {
if (!canUploadMedia || !treeContext.treeId || !treeContext.accessToken) throw new Error(t('Action non autorisée.'))
const type = resolveUploadMediaType(file, 'auto')
const validationError = validateMediaFile(file, type, {})
if (validationError) throw new Error(validationError)
try {
const created = await uploadPersonMedia(
treeContext.treeId,
personId,
{
type,
fileName: file.name || 'fichier',
mimeType: getUploadMimeType(file, type),
sizeBytes: file.size,
dataBase64: await fileToBase64(file),
caption: caption || null,
source: null,
lifeEventId,
...(submittedByLabel ? { submittedByLabel } : {}),
},
treeContext.accessToken,
)
return created
} catch (error) {
throw new Error(getAccountErrorMessage(error), { cause: error })
}
}
// Après l'enregistrement d'un moment : les nouveaux souvenirs apparaissent dans la fiche
const refreshMomentMedia = (personId) => refreshAfterMediaMutation(personId)
const handleMovePersonMedia = async (mediaId, direction) => {
if (!canEditCurrentTree || !selectedPerson || !treeContext.treeId || !treeContext.accessToken) {
return
}
const personId = selectedPerson.id
const orderedIds = selectedPersonMedias.map((media) => media.id)
const currentIndex = orderedIds.findIndex((id) => id === mediaId)
if (currentIndex < 0) {
return
}
const nextIndex = currentIndex + direction
if (nextIndex < 0 || nextIndex >= orderedIds.length) {
return
}
const reordered = [...orderedIds]
const [moved] = reordered.splice(currentIndex, 1)
reordered.splice(nextIndex, 0, moved)
setMediaManagerState((current) => ({
...current,
loading: true,
error: '',
}))
try {
await reorderPersonMedia(treeContext.treeId, personId, reordered, treeContext.accessToken)
await refreshAfterMediaMutation(personId)
setMediaManagerState((current) => ({
...current,
loading: false,
}))
} catch (error) {
setMediaManagerState((current) => ({
...current,
loading: false,
error: getAccountErrorMessage(error),
}))
}
}
const handleDeletePersonMedia = async (mediaId) => {
if (!canEditCurrentTree || !selectedPerson || !treeContext.treeId || !treeContext.accessToken) {
return
}
const personId = selectedPerson.id
setMediaManagerState((current) => ({
...current,
loading: true,
error: '',
}))
try {
await deletePersonMedia(treeContext.treeId, personId, mediaId, treeContext.accessToken)
await refreshAfterMediaMutation(personId)
setMediaManagerState((current) => ({
...current,
loading: false,
}))
} catch (error) {
setMediaManagerState((current) => ({
...current,
loading: false,
error: getAccountErrorMessage(error),
}))
}
}
// Modifier un souvenir (visionneuse) : le serveur refuse tout autre rôle que l'administrateur
const handleUpdatePersonMedia = async (personId, mediaId, payload) => {
if (!canEditCurrentTree || !treeContext.treeId || !treeContext.accessToken) {
throw new Error('forbidden')
}
await updatePersonMedia(treeContext.treeId, personId, mediaId, payload, treeContext.accessToken)
await refreshAfterMediaMutation(personId)
}
// Personnes identifiées sur un souvenir : administrateur, appliqué et arbre rafraîchi ; famille, une
// proposition (status « pending »), rien ne change avant la relecture
const handleUpdateMediaPeople = async (ownerPersonId, mediaId, people) => {
if (!canUploadMedia || !treeContext.treeId || !treeContext.accessToken) {
throw new Error('forbidden')
}
const name = canEditCurrentTree ? '' : readContributorName()
const result = await updateMediaPeople(
treeContext.treeId,
mediaId,
{ people, ...(name ? { submittedByLabel: name } : {}) },
treeContext.accessToken,
)
if (result?.status === 'approved') await refreshAfterMediaMutation(ownerPersonId)
return result
}
const loadPersonLifeEvents = (personId) => fetchPersonLife(treeContext.treeId, personId, treeContext.accessToken)
const resetMediaManager = () => {
setMediaManagerState({ visible: false, loading: false, error: '' })
}
return {
mediaManagerState,
selectedPersonMedias,
handleOpenMediaManager,
handleCloseMediaManager,
handleUploadPersonMedia,
handleMovePersonMedia,
uploadMomentFile,
refreshMomentMedia,
handleDeletePersonMedia,
handleUpdatePersonMedia,
handleUpdateMediaPeople,
loadPersonLifeEvents,
resetMediaManager,
}
}