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,