Isolate RNG drafts and fix image editor lifecycle
Some checks failed
Deploy Sokko G / deploy (push) Failing after 2s
Some checks failed
Deploy Sokko G / deploy (push) Failing after 2s
This commit is contained in:
parent
7c0b72f914
commit
b470a34b2a
7 changed files with 297 additions and 50 deletions
|
|
@ -43,17 +43,19 @@ export function ImageViewer({ image, onClose, createMarkerId = () => globalThis.
|
|||
const [eraseActive, setEraseActive] = useState(false);
|
||||
const [drawingColor, setDrawingColor] = useState("#f6c453");
|
||||
const [drawingWidth, setDrawingWidth] = useState(4);
|
||||
const [previousImage, setPreviousImage] = useState(image);
|
||||
const viewerRef = useRef(null);
|
||||
const mediaRef = useRef(null);
|
||||
const hasMarkers = viewerMarkers.length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (previousImage !== image) {
|
||||
setPreviousImage(image);
|
||||
setViewerMarkers(Array.isArray(image.markers) ? image.markers : []);
|
||||
setViewerDrawings(image.drawings?.strokes ? image.drawings : { strokes: [] });
|
||||
setDrawingMode(image.drawingMode === "temporary" ? "temporary" : "permanent");
|
||||
setDrawingActive(false);
|
||||
setEraseActive(false);
|
||||
}, [image]);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!viewerRef.current || !mediaRef.current) return undefined;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
// Rôle : édite une image utilisateur en cover rectangulaire de card toolbox.
|
||||
/* eslint-disable react-refresh/only-export-components -- expose aussi les helpers purs de calcul/export du crop. */
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Icon } from "../../components/Icon.jsx";
|
||||
import { useKeyedState } from "../../hooks/useKeyedState.js";
|
||||
import { lockBodyScroll } from "../../utils/bodyScrollLock.js";
|
||||
|
||||
const OUTPUT_WIDTH = 620;
|
||||
|
|
@ -64,46 +65,43 @@ export function ToolboxCoverEditor({ file, existingCover = "", onClose, onSave }
|
|||
const frameRef = useRef(null);
|
||||
const imageBitmapRef = useRef(null);
|
||||
const dragRef = useRef(null);
|
||||
const [source, setSource] = useState("");
|
||||
const [imageSize, setImageSize] = useState({ width: 0, height: 0 });
|
||||
const [preview, setPreview] = useKeyedState(file || existingCover, () => ({ source: "", width: 0, height: 0, error: "" }));
|
||||
const { source, error } = preview;
|
||||
const imageSize = preview;
|
||||
const [frame, setFrame] = useState({ width: 0, height: 0 });
|
||||
const [transform, setTransform] = useState({ x: 0, y: 0, scale: 1 });
|
||||
const [baseScale, setBaseScale] = useState(1);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const maxScale = useMemo(() => baseScale * 4, [baseScale]);
|
||||
const baseScale = frame.width && frame.height && imageSize.width && imageSize.height
|
||||
? Math.max(frame.width / imageSize.width, frame.height / imageSize.height) : 1;
|
||||
const transformKey = JSON.stringify([source, frame.width, frame.height, imageSize.width, imageSize.height]);
|
||||
const [transform, setTransform] = useKeyedState(transformKey, () => ({ x: 0, y: 0, scale: baseScale }));
|
||||
const maxScale = baseScale * 4;
|
||||
|
||||
useEffect(() => lockBodyScroll(), []);
|
||||
|
||||
useEffect(() => () => imageBitmapRef.current?.close?.(), []);
|
||||
|
||||
useEffect(() => {
|
||||
const nextSource = createPreviewSource(file, existingCover);
|
||||
setSource(nextSource);
|
||||
setError("");
|
||||
return () => {
|
||||
if (file && nextSource) URL.revokeObjectURL(nextSource);
|
||||
};
|
||||
}, [existingCover, file]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!source) return undefined;
|
||||
if (!nextSource) return undefined;
|
||||
let cancelled = false;
|
||||
decodePreviewImage(source, file)
|
||||
let decodedBitmap;
|
||||
decodePreviewImage(nextSource, file)
|
||||
.then((bitmap) => {
|
||||
if (cancelled) {
|
||||
bitmap.close?.();
|
||||
return;
|
||||
}
|
||||
imageBitmapRef.current?.close?.();
|
||||
decodedBitmap = bitmap;
|
||||
imageBitmapRef.current = bitmap;
|
||||
setImageSize({ width: bitmap.width, height: bitmap.height });
|
||||
setPreview({ source: nextSource, width: bitmap.width, height: bitmap.height, error: "" });
|
||||
})
|
||||
.catch(() => setError("Image illisible. Choisis un autre fichier."));
|
||||
.catch(() => {
|
||||
if (!cancelled) setPreview({ source: "", width: 0, height: 0, error: "Image illisible. Choisis un autre fichier." });
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
decodedBitmap?.close?.();
|
||||
if (imageBitmapRef.current === decodedBitmap) imageBitmapRef.current = null;
|
||||
if (file) URL.revokeObjectURL(nextSource);
|
||||
};
|
||||
}, [file, source]);
|
||||
}, [existingCover, file, setPreview]);
|
||||
|
||||
useEffect(() => {
|
||||
const frameElement = frameRef.current;
|
||||
|
|
@ -118,13 +116,6 @@ export function ToolboxCoverEditor({ file, existingCover = "", onClose, onSave }
|
|||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!frame.width || !frame.height || !imageSize.width || !imageSize.height) return;
|
||||
const nextBaseScale = Math.max(frame.width / imageSize.width, frame.height / imageSize.height);
|
||||
setBaseScale(nextBaseScale);
|
||||
setTransform({ x: 0, y: 0, scale: nextBaseScale });
|
||||
}, [frame.height, frame.width, imageSize.height, imageSize.width]);
|
||||
|
||||
function clampTransform(nextTransform) {
|
||||
const displayWidth = imageSize.width * nextTransform.scale;
|
||||
const displayHeight = imageSize.height * nextTransform.scale;
|
||||
|
|
|
|||
|
|
@ -165,10 +165,20 @@ export function RngSimulatorModule({ toolboxId, moduleId, context, editing = fal
|
|||
const [occurrenceDraft, setOccurrenceDraft] = useState(null);
|
||||
const [realDrawsDraft, setRealDrawsDraft] = useState(null);
|
||||
const rateFormRef = useRef(null);
|
||||
const previousEditingRef = useRef(editing);
|
||||
const [previousEditing, setPreviousEditing] = useState(editing);
|
||||
const rateDraft = useDraftForm({ label: "", rate: RNG_SIMULATOR_DEFAULT_RATE });
|
||||
const rateFormOpen = editing || Boolean(editingRateId);
|
||||
const activeRate = data.rates.find((rate) => rate.id === data.activeRateId) || data.rates[0] || null;
|
||||
const draftKey = JSON.stringify([toolboxId, moduleId, activeRate?.id, activeRate?.occurrenceUnit]);
|
||||
const [previousDraftKey, setPreviousDraftKey] = useState(null);
|
||||
if (previousDraftKey !== draftKey) {
|
||||
setPreviousDraftKey(draftKey);
|
||||
setMultiplierDraft(null);
|
||||
setCostDraft(null);
|
||||
setOccurrenceDraft(null);
|
||||
setRealDrawsDraft(null);
|
||||
setOccurrenceUnit(activeRate?.occurrenceUnit || "perHour");
|
||||
}
|
||||
const summary = useMemo(() => activeRate ? calculateRngSummary({
|
||||
rate: activeRate.rate,
|
||||
multiplier: activeRate.multiplier,
|
||||
|
|
@ -197,14 +207,11 @@ export function RngSimulatorModule({ toolboxId, moduleId, context, editing = fal
|
|||
const averageExceededDraws = Math.max(0, Math.round(averageDrawsDelta));
|
||||
const averageDrawsTone = realDrawsSummary?.averageExceeded ? (averageExceededDraws > 0 ? "negative" : "") : "positive";
|
||||
|
||||
useEffect(() => {
|
||||
const wasEditing = previousEditingRef.current;
|
||||
if (wasEditing && !editing) setEditingRateId("");
|
||||
if (!wasEditing && editing && !editingRateId) rateDraft.reset({ label: "", rate: RNG_SIMULATOR_DEFAULT_RATE });
|
||||
previousEditingRef.current = editing;
|
||||
// Le hook useDraftForm expose reset sans identité stable; cette transition ne doit dépendre que de l'ouverture.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [editing, editingRateId]);
|
||||
if (previousEditing !== editing) {
|
||||
setPreviousEditing(editing);
|
||||
if (!editing) setEditingRateId("");
|
||||
if (editing && !editingRateId) rateDraft.reset({ label: "", rate: RNG_SIMULATOR_DEFAULT_RATE });
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!rateFormOpen) return undefined;
|
||||
|
|
@ -219,14 +226,6 @@ export function RngSimulatorModule({ toolboxId, moduleId, context, editing = fal
|
|||
return () => document.removeEventListener("pointerdown", closeOnOutsidePointer);
|
||||
}, [rateFormOpen, setEditing]);
|
||||
|
||||
useEffect(() => {
|
||||
setMultiplierDraft(null);
|
||||
setCostDraft(null);
|
||||
setOccurrenceDraft(null);
|
||||
setRealDrawsDraft(null);
|
||||
setOccurrenceUnit(activeRate?.occurrenceUnit || "perHour");
|
||||
}, [activeRate?.id, activeRate?.occurrenceUnit]);
|
||||
|
||||
function save(nextData) {
|
||||
context.setModuleData(toolboxId, moduleId, nextData, "rngSimulator");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue