From b94f5f44cbfd02cd793acb117b92d7ef72141f64 Mon Sep 17 00:00:00 2001 From: taDuc Date: Thu, 28 May 2026 18:24:04 +0700 Subject: [PATCH] refactor: optimize replay preview state management and cleanup editor UI components --- src/app/editor/[id]/page.tsx | 10 -- src/app/page.tsx | 147 +++++++++++++++++- .../components/editor/PresentPlaceSearch.tsx | 3 + src/uhm/lib/replay/useReplayPreview.ts | 127 ++++++++------- 4 files changed, 214 insertions(+), 73 deletions(-) diff --git a/src/app/editor/[id]/page.tsx b/src/app/editor/[id]/page.tsx index a673339..24c2c23 100644 --- a/src/app/editor/[id]/page.tsx +++ b/src/app/editor/[id]/page.tsx @@ -15,7 +15,6 @@ import WikiSidebarPanel from "@/uhm/components/wiki/WikiSidebarPanel"; import ProjectEntityRefsPanel from "@/uhm/components/editor/ProjectEntityRefsPanel"; import EntityWikiBindingsPanel from "@/uhm/components/editor/EntityWikiBindingsPanel"; import GeometryBindingPanel from "@/uhm/components/editor/GeometryBindingPanel"; -import ImageOverlayPanel from "@/uhm/components/editor/ImageOverlayPanel"; import { Entity, fetchEntities, searchEntitiesByName } from "@/uhm/api/entities"; import { ApiError } from "@/uhm/api/http"; import { fetchCurrentUser } from "@/uhm/api/auth"; @@ -2781,15 +2780,6 @@ function EditorPageContent() { isGeoSearching={isGeoSearching} onImportGeoFromSearch={handleImportGeoFromSearch} /> - ("idle"); const [selectedReplayStageId, setSelectedReplayStageId] = useState(null); const [selectedReplayStepIndex, setSelectedReplayStepIndex] = useState(null); + const [focusedPresentPlace, setFocusedPresentPlace] = useState(null); const [searchTimelineYear, setSearchTimelineYear] = useState(timelineYear); useEffect(() => { @@ -102,19 +109,22 @@ export default function Page() { return null; }, [replays, selectedFeatureIds]); + const getMapInstance = useCallback(() => mapHandleRef.current?.getMap() || null, []); + const handleSelectReplayStep = useCallback((stageId: number | null, stepIndex: number | null) => { + setSelectedReplayStageId(stageId); + setSelectedReplayStepIndex(stepIndex); + }, []); + const replayPreview = useReplayPreview({ replay: activeReplay?.replay || null, draft: renderDraft, - getMapInstance: () => mapHandleRef.current?.getMap() || null, + getMapInstance, initialTimelineYear: timelineDraftYear, initialTimelineFilterEnabled: false, initialMapViewState: null, selectedStageId: selectedReplayStageId, selectedStepIndex: selectedReplayStepIndex, - onSelectStep: (stageId, stepIndex) => { - setSelectedReplayStageId(stageId); - setSelectedReplayStepIndex(stepIndex); - }, + onSelectStep: handleSelectReplayStep, }); const { @@ -205,8 +215,60 @@ export default function Page() { const handleExitReplay = useCallback(() => { setReplayMode("idle"); replayPreview.resetPreview(); + setFocusedPresentPlace(null); }, [replayPreview]); + const handleFocusPresentPlace = useCallback((place: PresentPlaceSelection) => { + setFocusedPresentPlace(place); + const map = mapHandleRef.current?.getMap(); + if (map) { + const currentZoom = map.getZoom(); + map.flyTo({ + center: [place.lng, place.lat], + zoom: Math.max(currentZoom, 13.5), + }); + } + }, []); + + const clearPresentPlaceFocus = useCallback(() => { + setFocusedPresentPlace(null); + }, []); + + const handleFocusHistoricalGeometry = useCallback((payload: HistoricalGeometryFocusPayload) => { + setFocusedPresentPlace(null); + + const map = mapHandleRef.current?.getMap(); + if (map && payload.geometry?.draw_geometry) { + const fc: FeatureCollection = { + type: "FeatureCollection", + features: [ + { + type: "Feature", + properties: { + id: payload.geometry.id, + }, + geometry: payload.geometry.draw_geometry, + }, + ], + }; + fitMapToFeatureCollection(map, fc, 84, { duration: 1000 }); + } + + if (payload.geometry.time_start != null) { + handleTimelineYearChange(payload.geometry.time_start); + } + + setSelectedFeatureIds([payload.geometry.id]); + + const linkedEntityIds = relations.geometryEntityIds[String(payload.geometry.id)] || []; + if (linkedEntityIds.length === 1) { + selectEntity(linkedEntityIds[0], { + sourceFeatureId: payload.geometry.id, + selectGeometry: false, + }); + } + }, [relations.geometryEntityIds, selectEntity, setSelectedFeatureIds]); + const filteredRenderDraft = useMemo(() => { if (replayMode !== "playing" || !replayPreview.hiddenGeometryIds?.length) { return renderDraft; @@ -294,7 +356,80 @@ export default function Page() { /> ) : null } - /> + > +
+ + + +
+ ) : (
)} diff --git a/src/uhm/components/editor/PresentPlaceSearch.tsx b/src/uhm/components/editor/PresentPlaceSearch.tsx index 9a02372..c44a29a 100644 --- a/src/uhm/components/editor/PresentPlaceSearch.tsx +++ b/src/uhm/components/editor/PresentPlaceSearch.tsx @@ -35,6 +35,7 @@ type Props = { onFocusHistoricalGeometry: (payload: HistoricalGeometryFocusPayload) => void; onClearFocus: () => void; leftOffset?: number; + style?: CSSProperties; }; export default function PresentPlaceSearch({ @@ -43,6 +44,7 @@ export default function PresentPlaceSearch({ onFocusHistoricalGeometry, onClearFocus, leftOffset = 18, + style, }: Props) { const [mode, setMode] = useState("present"); const [query, setQuery] = useState(""); @@ -284,6 +286,7 @@ export default function PresentPlaceSearch({ zIndex: 18, width: "min(392px, calc(100vw - 36px))", pointerEvents: "auto", + ...style, }} onMouseDown={(event) => event.stopPropagation()} > diff --git a/src/uhm/lib/replay/useReplayPreview.ts b/src/uhm/lib/replay/useReplayPreview.ts index bc9c85f..477d0b1 100644 --- a/src/uhm/lib/replay/useReplayPreview.ts +++ b/src/uhm/lib/replay/useReplayPreview.ts @@ -93,6 +93,13 @@ export function useReplayPreview({ const baselineRef = useRef(null); const effects = useMemo(() => createReplayMapEffects(), []); + const selectedStageIdRef = useRef(selectedStageId); + const selectedStepIndexRef = useRef(selectedStepIndex); + useEffect(() => { + selectedStageIdRef.current = selectedStageId; + selectedStepIndexRef.current = selectedStepIndex; + }, [selectedStageId, selectedStepIndex]); + const flatSteps = useMemo(() => flattenReplaySteps(replay), [replay]); useEffect(() => { @@ -203,61 +210,60 @@ export function useReplayPreview({ }, [restorePreviewState]); const controllersRef = useRef[0] | null>(null); - useEffect(() => { - controllersRef.current = { - map: getMapInstance(), - draft, - effects, - setTimelineVisible, - setTimelineFilterEnabled, - setLayerPanelVisible, - setZoomPanelVisible, - setSidebarOpen, - onSelectWiki: (id) => { - const nextId = String(id || "").trim(); - setActiveWikiId(nextId || null); - }, - addToast, - setPlaybackSpeed: (nextSpeed) => { - const safe = Number.isFinite(nextSpeed) && nextSpeed > 0 ? nextSpeed : 1; - playbackSpeedRef.current = safe; - setPlaybackSpeed(safe); - }, - onYearChange: setTimelineYear, - showGeometries: (ids) => { - const nextIds = normalizeIdList(ids); - if (!nextIds.length) return; - setHiddenGeometryIds((prev) => prev.filter((id) => !nextIds.includes(id))); - }, - hideGeometries: (ids) => { - const nextIds = normalizeIdList(ids); - if (!nextIds.length) return; - setHiddenGeometryIds((prev) => { - const seen = new Set(prev); - for (const id of nextIds) { - seen.add(id); - } - return Array.from(seen); - }); - }, - showOnlyGeometries: (ids) => { - const keepIds = new Set(normalizeIdList(ids)); - if (!keepIds.size) return; - setHiddenGeometryIds( - draft.features - .map((feature) => String(feature.properties.id)) - .filter((id) => !keepIds.has(id)) - ); - }, - showAllGeometries: () => { - setHiddenGeometryIds([]); - }, - setDialog: setDialogWithRef, - getDialog: () => dialogRef.current, - }; - }, [addToast, draft, effects, getMapInstance]); + controllersRef.current = { + map: getMapInstance(), + draft, + effects, + setTimelineVisible, + setTimelineFilterEnabled, + setLayerPanelVisible, + setZoomPanelVisible, + setSidebarOpen, + onSelectWiki: (id) => { + const nextId = String(id || "").trim(); + setActiveWikiId(nextId || null); + }, + addToast, + setPlaybackSpeed: (nextSpeed) => { + const safe = Number.isFinite(nextSpeed) && nextSpeed > 0 ? nextSpeed : 1; + playbackSpeedRef.current = safe; + setPlaybackSpeed(safe); + }, + onYearChange: setTimelineYear, + showGeometries: (ids) => { + const nextIds = normalizeIdList(ids); + if (!nextIds.length) return; + setHiddenGeometryIds((prev) => prev.filter((id) => !nextIds.includes(id))); + }, + hideGeometries: (ids) => { + const nextIds = normalizeIdList(ids); + if (!nextIds.length) return; + setHiddenGeometryIds((prev) => { + const seen = new Set(prev); + for (const id of nextIds) { + seen.add(id); + } + return Array.from(seen); + }); + }, + showOnlyGeometries: (ids) => { + const keepIds = new Set(normalizeIdList(ids)); + if (!keepIds.size) return; + setHiddenGeometryIds( + draft.features + .map((feature) => String(feature.properties.id)) + .filter((id) => !keepIds.has(id)) + ); + }, + showAllGeometries: () => { + setHiddenGeometryIds([]); + }, + setDialog: setDialogWithRef, + getDialog: () => dialogRef.current, + }; const playFromIndex = useCallback(async (startIndex: number) => { + console.log("playFromIndex starting at:", startIndex, "flatSteps count:", flatSteps.length); if (!flatSteps.length) return; const safeStartIndex = Math.max(0, Math.min(flatSteps.length - 1, startIndex)); resetPresentation(); @@ -271,7 +277,10 @@ export function useReplayPreview({ setIsPlaying(true); for (let index = safeStartIndex; index < flatSteps.length; index += 1) { - if (runIdRef.current !== runId) return; + if (runIdRef.current !== runId) { + console.log("playFromIndex loop aborted because runId changed"); + return; + } const current = flatSteps[index]; setActiveCursor({ @@ -282,7 +291,10 @@ export function useReplayPreview({ onSelectStep(current.stageId, current.stepIndex); const controllers = controllersRef.current; - if (!controllers) return; + if (!controllers) { + console.warn("playFromIndex aborted: controllersRef.current is null!"); + return; + } controllers.map = getMapInstance(); controllers.draft = draft; @@ -322,9 +334,10 @@ export function useReplayPreview({ }, [playFromIndex]); const playFromSelection = useCallback(() => { - const selectedIndex = findReplayStepIndex(flatSteps, selectedStageId, selectedStepIndex); + const selectedIndex = findReplayStepIndex(flatSteps, selectedStageIdRef.current, selectedStepIndexRef.current); + console.log("playFromSelection called: selectedIndex =", selectedIndex, "selectedStageId =", selectedStageIdRef.current, "selectedStepIndex =", selectedStepIndexRef.current); void playFromIndex(selectedIndex >= 0 ? selectedIndex : 0); - }, [flatSteps, playFromIndex, selectedStageId, selectedStepIndex]); + }, [flatSteps, playFromIndex]); return { isPlaying,