diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index e6a1407..6c18939 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -51,7 +51,7 @@ import {
reorderSiblings,
uploadPersonMedia,
} from './api/treeApi'
-import { fetchImmichConfig, getImmichErrorMessage, unassignImmichPerson } from './api/immichApi.js'
+import { getImmichErrorMessage, unassignImmichPerson } from './api/immichApi.js'
import { fetchAuthOptions, oidcStartUrl } from './api/authApi'
import { useAuth } from './hooks/useAuth'
import { useTreeAccess, getTreeTokenStorageKey, getSlugFromPathname, readTreeAccessRole, TREE_ID_FROM_ENV } from './hooks/useTreeAccess'
@@ -1715,51 +1715,6 @@ function App() {
}
}, [auth.userAuth.token, tree.treeContext.accessToken, tree.treeContext.treeId, refreshTreeAndKeepSelection])
- // « + photo » dans l'arbre : le sélecteur part dans le clic (Safari iOS refuse un input file ouvert hors geste),
- // puis la fenêtre de recadrage s'ouvre dans la fiche de la personne
- const portraitInputRef = useRef(null)
- const portraitPickerPersonIdRef = useRef(null)
- const [pendingPortraitCrop, setPendingPortraitCrop] = useState(null)
- const [photoSourceRequest, setPhotoSourceRequest] = useState(null)
- const [immichSourceReady, setImmichSourceReady] = useState(false)
-
- // Administrateur : Immich proposé dans le menu des sources seulement s'il est configuré
- useEffect(() => {
- let cancelled = false
- const treeId = tree.treeContext.treeId
- const token = tree.treeContext.accessToken || auth.userAuth.token
- if (!userRole.isAdmin || !tree.canEditCurrentTree || !treeId || !token) {
- setImmichSourceReady(false)
- return undefined
- }
- fetchImmichConfig(treeId, token)
- .then(({ config }) => { if (!cancelled) setImmichSourceReady(Boolean(config?.configured)) })
- .catch(() => { if (!cancelled) setImmichSourceReady(false) })
- return () => { cancelled = true }
- }, [userRole.isAdmin, tree.canEditCurrentTree, tree.treeContext.treeId, tree.treeContext.accessToken, auth.userAuth.token])
-
- const handlePhotoPlaceholderClick = useCallback((personId) => {
- // En modification avec Immich : le menu des sources s'ouvre dans la fiche (le badge en est l'ancre)
- if (editMode.isActive && immichSourceReady) {
- setPhotoSourceRequest({ personId })
- } else {
- // Sans menu : le sélecteur de fichiers part tout de suite, dans le clic
- portraitPickerPersonIdRef.current = personId
- portraitInputRef.current?.click()
- }
- handlePersonSelect(personId)
- }, [editMode.isActive, immichSourceReady, handlePersonSelect])
-
- const handlePortraitPicked = useCallback(async (event) => {
- const file = event.target.files?.[0]
- event.target.value = ''
- const personId = portraitPickerPersonIdRef.current
- portraitPickerPersonIdRef.current = null
- if (!file || personId === null) return
- const result = await handlePersonAvatarUpload(personId, file)
- if (result?.previewUrl) setPendingPortraitCrop({ personId, src: result.previewUrl })
- }, [handlePersonAvatarUpload])
-
// Visage venu d'Immich : recharger l'arbre pour montrer le nouveau portrait (ou le portrait restant)
const handleImmichChange = useCallback(async (personId) => {
await refreshTreeAndKeepSelection(personId, { relayout: false, avatarCacheBust: Date.now() })
@@ -3244,17 +3199,11 @@ function App() {
zoomApiRef={galaxyZoomApiRef}
focusPersonId={tree.treeContext.rootPersonId}
assetRevision={tree.assetRevision}
- // « + photo » seulement en modification : en lecture, un clic sur un portrait vide ouvre la fiche (issue #124)
- photoHint={tree.canEditCurrentTree && editMode.isActive}
- onPhotoPlaceholderClick={tree.canEditCurrentTree && editMode.isActive ? handlePhotoPlaceholderClick : null}
reorderEnabled={canReorderSiblings}
onReorderSiblings={handleReorderSiblings}
familyTabs={familyTabs}
/>
- {tree.canEditCurrentTree && (
-
- )}
{tree.selectedPerson && (
setPendingPortraitCrop(null)}
- photoSourceRequest={photoSourceRequest}
- onPhotoSourceRequestDone={() => setPhotoSourceRequest(null)}
treeId={tree.treeContext.treeId || null}
immichToken={tree.treeContext.accessToken || auth.userAuth.token}
onImmichChange={tree.canEditCurrentTree ? handleImmichChange : undefined}
diff --git a/frontend/src/components/Galaxy.jsx b/frontend/src/components/Galaxy.jsx
index 20e168c..d14f9a7 100644
--- a/frontend/src/components/Galaxy.jsx
+++ b/frontend/src/components/Galaxy.jsx
@@ -42,7 +42,7 @@ const DROP_SPRING = { stiffness: 480, dampingRatio: 0.72 }
const centerX = (node) => node.x + node.width / 2
const prefersReducedMotion = () => typeof window !== 'undefined' && window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
-const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlightIds = [], graphRevision = 0, annotationHandlers = null, transformReadRef = null, zoomApiRef = null, focusPersonId = null, assetRevision = 0, photoHint = false, onPhotoPlaceholderClick = null, reorderEnabled = false, onReorderSiblings = null, familyTabs = true }) => {
+const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlightIds = [], graphRevision = 0, annotationHandlers = null, transformReadRef = null, zoomApiRef = null, focusPersonId = null, assetRevision = 0, reorderEnabled = false, onReorderSiblings = null, familyTabs = true }) => {
const canvasRef = useRef(null)
const layoutDataRef = useRef(null)
// Onglets de familles (réglage `familyTabs`) : la liste des familles de l'arbre, calculée une fois par
@@ -78,8 +78,6 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig
// Clic sur un badge : l'onglet de cette famille (ou, depuis un onglet, celui d'où l'on vient)
const openFamily = useCallback((tabKey) => { if (tabKey != null) setActiveKey(String(tabKey)) }, [])
// Propriétaire : un portrait vide survolé montre « + photo »
- const photoHintRef = useRef(photoHint)
- useEffect(() => { photoHintRef.current = photoHint }, [photoHint])
const [layoutReady, setLayoutReady] = useState(false)
const transformRef = useRef({ x: 0, y: 0, scale: 1 })
@@ -169,7 +167,7 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig
canvasRef, transformRef, targetTransformRef,
layoutDataRef, nodeRandomDataRef, entranceRef,
hoveredNodeIdRef: hoveredNodeId, hoveredMediaKeyRef: hoveredMediaKey, mouseWorldPosRef: mouseWorldPos,
- getAnimatedPos, onPersonSelect, onMediaSelect, onPhotoPlaceholderClick, onOpenFamily: openFamily,
+ getAnimatedPos, onPersonSelect, onMediaSelect, onOpenFamily: openFamily,
annotationHandlers,
minScale: MIN_SCALE,
maxScale: MAX_SCALE,
@@ -907,7 +905,6 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig
mediaHoverState: mediaHoverState.current,
entrance, entranceActive, elapsed,
view,
- photoHint: photoHintRef.current,
liftScales: liftScalesRef.current,
})
drawRenvoiPills(ctx)
diff --git a/frontend/src/components/PersonCard.jsx b/frontend/src/components/PersonCard.jsx
index 0d6e016..ee7b2e2 100644
--- a/frontend/src/components/PersonCard.jsx
+++ b/frontend/src/components/PersonCard.jsx
@@ -54,10 +54,6 @@ const PersonCard = ({
onAvatarUpload,
onAvatarDelete,
onAvatarCrop,
- pendingPortraitCrop = null,
- onPendingPortraitCropDone = null,
- photoSourceRequest = null,
- onPhotoSourceRequestDone = null,
treeId = null,
immichToken = null,
onImmichChange,
@@ -258,21 +254,6 @@ const PersonCard = ({
const chooseFileSource = () => { setShowSourceMenu(false); avatarInputRef.current?.click() }
const chooseImmichSource = () => { setShowSourceMenu(false); setShowImmichPanel(true) }
- // Clic sur « + photo » dans l'arbre : la photo vient d'être envoyée, le recadrage s'ouvre tout de suite
- useEffect(() => {
- if (!pendingPortraitCrop || String(pendingPortraitCrop.personId) !== String(person.id)) return
- setCropState({ src: pendingPortraitCrop.src, mode: 'new', ownsUrl: true })
- onPendingPortraitCropDone?.()
- }, [pendingPortraitCrop, person.id, onPendingPortraitCropDone])
-
- // Clic sur « + photo » dans l'arbre, en modification avec Immich : même menu que le badge, sans clic de plus
- useEffect(() => {
- if (!photoSourceRequest || String(photoSourceRequest.personId) !== String(person.id)) return
- if (immichConfigured === null) return
- onPhotoSourceRequestDone?.()
- if (immichUsable) openAvatarPicker()
- }, [photoSourceRequest, person.id, immichConfigured, immichUsable, onPhotoSourceRequestDone]) // eslint-disable-line react-hooks/exhaustive-deps
-
// Focus search input when relation search opens
useEffect(() => {
if (activeRelationSearch) {
diff --git a/frontend/src/components/galaxy/renderers.js b/frontend/src/components/galaxy/renderers.js
index 19ade18..a43583b 100644
--- a/frontend/src/components/galaxy/renderers.js
+++ b/frontend/src/components/galaxy/renderers.js
@@ -203,8 +203,6 @@ export function drawCoupleLinks(ctx, coupleBarMeta, posMap, nodeMap, nodeRandomD
export const renvoiHitAreas = []
// Badges « Famille X » sous les conjoints dont la famille est un onglet : { x0, y0, x1, y1, anchorId }
export const familyHitAreas = []
-// Zones « + photo » : portraits vides, chez le propriétaire en modification (cercle du nœud, rempli au dessin)
-export const photoPlaceholderHitAreas = []
const renvoiPills = []
const familyPills = [] // badges « Famille X » : { text, cx, cy, anchorId, tabKey, alpha }
const RENVOI_FONT = '500 11px "DM Sans", system-ui, sans-serif'
@@ -464,15 +462,11 @@ function measureTextCached(ctx, text, fontKey) {
// ============================================================
function drawPersonNode(ctx, node, pos, nodeProgress, {
nodeRandomData, imageCache, hoverScales, selectedGlowScales, searchGlowScales,
- mediaHoverState, entrance, entranceActive, elapsed, view = null, photoHint = false, liftScales = null,
+ mediaHoverState, entrance, entranceActive, elapsed, view = null, liftScales = null,
}) {
const { cx, cy } = pos
const person = getPersonById(node._data.id) || node._data
const rd = nodeRandomData.get(node.id)
- // Zone « + photo » : seulement là où le cadre invite à poser une photo (même condition que le dessin)
- if (photoHint && !person.photo && nodeProgress >= 0.5) {
- photoPlaceholderHitAreas.push({ personId: node._data.id, cx, cy, r: nodeRadius(node) })
- }
const img = person.photo ? imageCache.get(person.photo) : null
const frameType = resolveFrameType(person, img)
const frameRot = rd?.frameRotation || 0
@@ -606,7 +600,7 @@ function drawPersonNode(ctx, node, pos, nodeProgress, {
ctx.shadowOffsetY = shadowY
}
- drawFrame(ctx, frameType, person, img, photoHint && !img ? hs : 0)
+ drawFrame(ctx, frameType, person, img)
ctx.restore()
// --- Labels (estompés puis absents en dézoom) ---
@@ -932,7 +926,7 @@ function resolveFrameType(person, img) {
return resolveFrameKind(person.frameType || 'round', ratio, cropShape(person.avatarCrop))
}
-function drawFrame(ctx, frameType, person, img, hint = 0) {
+function drawFrame(ctx, frameType, person, img) {
if (frameType === 'polaroid') {
const pw = PERSON_R + 4
const ph = PERSON_R + 4
@@ -950,7 +944,7 @@ function drawFrame(ctx, frameType, person, img, hint = 0) {
ctx.beginPath()
ctx.roundRect(-pw, -ph, pw * 2, ph * 2, 2)
ctx.clip()
- drawPersonImage(ctx, img, person, -pw, -ph, pw * 2, ph * 2, hint)
+ drawPersonImage(ctx, img, person, -pw, -ph, pw * 2, ph * 2)
} else if (frameType === 'square') {
const s = PERSON_R + 4
const pad = 3
@@ -966,7 +960,7 @@ function drawFrame(ctx, frameType, person, img, hint = 0) {
ctx.beginPath()
ctx.roundRect(-s, -s, s * 2, s * 2, 2)
ctx.clip()
- drawPersonImage(ctx, img, person, -s, -s, s * 2, s * 2, hint)
+ drawPersonImage(ctx, img, person, -s, -s, s * 2, s * 2)
} else if (frameType === 'rect') {
const ratio = img?.naturalWidth && img?.naturalHeight
? img.naturalWidth / img.naturalHeight
@@ -988,7 +982,7 @@ function drawFrame(ctx, frameType, person, img, hint = 0) {
ctx.beginPath()
ctx.roundRect(-rw, -rh, rw * 2, rh * 2, 3)
ctx.clip()
- drawPersonImage(ctx, img, person, -rw, -rh, rw * 2, rh * 2, hint)
+ drawPersonImage(ctx, img, person, -rw, -rh, rw * 2, rh * 2)
} else {
ctx.fillStyle = '#FFFFFF'
ctx.beginPath()
@@ -1003,14 +997,14 @@ function drawFrame(ctx, frameType, person, img, hint = 0) {
ctx.beginPath()
ctx.arc(0, 0, PERSON_R, 0, Math.PI * 2)
ctx.clip()
- drawPersonImage(ctx, img, person, -PERSON_R, -PERSON_R, PERSON_R * 2, PERSON_R * 2, hint)
+ drawPersonImage(ctx, img, person, -PERSON_R, -PERSON_R, PERSON_R * 2, PERSON_R * 2)
}
}
// ============================================================
// Dessiner l'image ou le placeholder initiales
// ============================================================
-function drawPersonImage(ctx, img, person, x, y, w, h, hint = 0) {
+function drawPersonImage(ctx, img, person, x, y, w, h) {
if (img) {
drawImageCover(ctx, img, x, y, w, h, person.avatarCrop)
if (!person.isAlive) {
@@ -1022,25 +1016,9 @@ function drawPersonImage(ctx, img, person, x, y, w, h, hint = 0) {
ctx.fillRect(x, y, w, h)
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
- // Au survol, chez le propriétaire en modification, les initiales cèdent la place à « + photo »
- const baseAlpha = ctx.globalAlpha
- if (hint > 0.01) {
- ctx.fillStyle = `rgba(248, 228, 219, ${hint})`
- ctx.fillRect(x, y, w, h)
- }
- ctx.globalAlpha = baseAlpha * (1 - hint)
ctx.fillStyle = COLORS.text
ctx.font = '400 16px "Newsreader", Georgia, serif'
ctx.fillText((person.firstName[0] || '') + (person.lastName[0] || ''), 0, 0)
- if (hint > 0.01) {
- ctx.globalAlpha = baseAlpha * hint
- ctx.fillStyle = '#93402A'
- ctx.font = '300 22px "DM Sans", system-ui, sans-serif'
- ctx.fillText('+', 0, -6)
- ctx.font = '500 9.5px "DM Sans", system-ui, sans-serif'
- ctx.fillText(t('photo'), 0, 10)
- }
- ctx.globalAlpha = baseAlpha
}
}
@@ -1062,7 +1040,6 @@ function drawImageCover(ctx, img, x, y, w, h, crop = null) {
// ============================================================
export function drawNodes(ctx, layoutData, posMap, entrance, entranceActive, elapsed, renderState) {
const view = renderState.view || null
- photoPlaceholderHitAreas.length = 0
layoutData.children.forEach(node => {
const pos = posMap.get(node.id)
if (!pos) return
diff --git a/frontend/src/components/galaxy/useGalaxyInteractions.js b/frontend/src/components/galaxy/useGalaxyInteractions.js
index 6df39cb..6f3e64c 100644
--- a/frontend/src/components/galaxy/useGalaxyInteractions.js
+++ b/frontend/src/components/galaxy/useGalaxyInteractions.js
@@ -1,11 +1,10 @@
import { useRef, useCallback } from 'react'
import { getPersonMedias } from '../../data/mockData'
import { MAX_ORBIT_MEDIAS, ORBIT_MEDIA_SIZE } from './constants'
-import { nodeRadius, renvoiHitAreas, familyHitAreas, photoPlaceholderHitAreas } from './renderers'
+import { nodeRadius, renvoiHitAreas, familyHitAreas } from './renderers'
const hitRenvoi = (x, y) => renvoiHitAreas.find(a => x >= a.x0 && x <= a.x1 && y >= a.y0 && y <= a.y1)
const hitFamily = (x, y) => familyHitAreas.find(a => x >= a.x0 && x <= a.x1 && y >= a.y0 && y <= a.y1)
-const hitPhotoPlaceholder = (x, y) => photoPlaceholderHitAreas.find(a => Math.hypot(x - a.cx, y - a.cy) < a.r)
import { getEntranceProgress } from './utils'
const CURSOR_GRAB = 'default'
@@ -23,7 +22,7 @@ export function useGalaxyInteractions({
canvasRef, transformRef, targetTransformRef,
layoutDataRef, nodeRandomDataRef, entranceRef,
hoveredNodeIdRef, hoveredMediaKeyRef, mouseWorldPosRef,
- getAnimatedPos, onPersonSelect, onMediaSelect, onPhotoPlaceholderClick = null, onOpenFamily = null,
+ getAnimatedPos, onPersonSelect, onMediaSelect, onOpenFamily = null,
annotationHandlers,
minScale = 0.2,
maxScale = 4,
@@ -140,7 +139,6 @@ export function useGalaxyInteractions({
}
}
if (!isOverClickable && (hitRenvoi(mouseWorldPosRef.current.x, mouseWorldPosRef.current.y) || hitFamily(mouseWorldPosRef.current.x, mouseWorldPosRef.current.y))) isOverClickable = true
- if (!isOverClickable && hitPhotoPlaceholder(mouseWorldPosRef.current.x, mouseWorldPosRef.current.y)) isOverClickable = true
hoveredNodeIdRef.current = newHoveredNode
hoveredMediaKeyRef.current = newHoveredMedia
@@ -265,13 +263,6 @@ export function useGalaxyInteractions({
}
}
- // « + photo » : le clic lance directement la photo (la fiche s'ouvre aussi, côté App)
- const photoZone = hitPhotoPlaceholder(clickX, clickY)
- if (photoZone && onPhotoPlaceholderClick) {
- onPhotoPlaceholderClick(photoZone.personId)
- return
- }
-
for (const node of layoutData.children) {
if (node._type !== 'person') continue
if (ent && !ent.finished) {
@@ -300,7 +291,7 @@ export function useGalaxyInteractions({
y: rect2.height / 2 - renvoi.targetY * scale,
}
}
- }, [canvasRef, transformRef, targetTransformRef, layoutDataRef, nodeRandomDataRef, entranceRef, getAnimatedPos, onPersonSelect, onMediaSelect, onPhotoPlaceholderClick, onOpenFamily, annotationHandlers])
+ }, [canvasRef, transformRef, targetTransformRef, layoutDataRef, nodeRandomDataRef, entranceRef, getAnimatedPos, onPersonSelect, onMediaSelect, onOpenFamily, annotationHandlers])
return {
isDragging, isZooming, zoomTimeoutRef, hasDragged,