fix(lightbox): anchor save hint below rendered image

Measure the image's rendered bottom edge with refs + ResizeObserver and
position the long-press save hint relative to it instead of pinning to
screen center or stage bottom. Enlarges the toast for mobile legibility
and clamps the offset so tall portrait images don't push it offscreen.
This commit is contained in:
TerryM
2026-06-02 10:51:17 +08:00
parent 8acb3a281b
commit 6c0c3b89a9

View File

@@ -152,7 +152,38 @@ function LightboxView({
const [index, setIndex] = useState(startIndex);
const [isCaptionVisible, setIsCaptionVisible] = useState(true);
const [showSaveHint, setShowSaveHint] = useState(true);
const [hintTop, setHintTop] = useState<number | null>(null);
const touchStartX = useRef<number | null>(null);
const stageRef = useRef<HTMLDivElement | null>(null);
const imgRef = useRef<HTMLImageElement | null>(null);
const measureHintPosition = useCallback(() => {
const stage = stageRef.current;
const img = imgRef.current;
if (!stage || !img) return;
const stageRect = stage.getBoundingClientRect();
const imgRect = img.getBoundingClientRect();
const gap = 12;
const safeBottomReserve = 80;
const desired = imgRect.bottom - stageRect.top + gap;
const maxTop = stageRect.height - safeBottomReserve;
setHintTop(Math.max(0, Math.min(desired, maxTop)));
}, []);
useEffect(() => {
if (!showSaveHint) return;
measureHintPosition();
const img = imgRef.current;
const stage = stageRef.current;
const ro = new ResizeObserver(() => measureHintPosition());
if (img) ro.observe(img);
if (stage) ro.observe(stage);
window.addEventListener("resize", measureHintPosition);
return () => {
ro.disconnect();
window.removeEventListener("resize", measureHintPosition);
};
}, [measureHintPosition, showSaveHint, index]);
// Clamp at the ends instead of wrapping; the nav arrows / swipe / arrow
// keys should all behave like a linear gallery, not a carousel.
@@ -241,7 +272,10 @@ function LightboxView({
</div>
{/* Image stage */}
<div className="relative flex min-h-0 w-full flex-1 items-center justify-center">
<div
ref={stageRef}
className="relative flex min-h-0 w-full flex-1 items-center justify-center"
>
{hasMany && index > 0 ? (
<button
type="button"
@@ -270,25 +304,26 @@ function LightboxView({
) : null}
{showSaveHint ? (
<div className="pointer-events-none fixed inset-x-0 top-[58%] z-30 flex -translate-y-1/2 justify-center px-4">
<div
className="pointer-events-none absolute inset-x-0 z-30 flex justify-center px-4"
style={hintTop != null ? { top: hintTop } : { bottom: 16 }}
>
<div
className="pointer-events-auto relative flex w-44 max-w-[calc(100vw-2rem)] flex-col items-center gap-1.5 rounded-xl bg-black/70 px-4 py-3 text-center text-white shadow-2xl ring-1 ring-white/20 backdrop-blur-md animate-scale-in"
className="pointer-events-auto relative flex max-w-[calc(100vw-2rem)] items-center gap-3 rounded-full bg-black/75 px-5 py-3 text-white shadow-2xl ring-1 ring-white/20 backdrop-blur-md animate-scale-in"
onClick={(e) => e.stopPropagation()}
>
<span className="text-2xl leading-none animate-bounce"></span>
<span className="text-sm font-semibold leading-5">
{t("longPressImageSave")}
</span>
<button
type="button"
onClick={() => setShowSaveHint(false)}
className="absolute right-1.5 top-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-white/10 text-white/80 transition hover:bg-white/20 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70"
className="flex h-7 w-7 items-center justify-center rounded-full bg-white/10 text-white/80 transition hover:bg-white/20 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70"
aria-label="Close hint"
>
<X className="h-3.5 w-3.5" />
<X className="h-4 w-4" />
</button>
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-white/15 text-2xl animate-bounce">
</div>
<div className="max-w-full break-words text-xs font-medium leading-4">
{t("longPressImageSave")}
</div>
</div>
</div>
) : null}
@@ -315,8 +350,10 @@ function LightboxView({
{/* No select-none / touch-callout:none here so iOS Safari's native
long-press menu ("Save in Photos") works on the full-size image. */}
<img
ref={imgRef}
src={current.url}
alt={current.filename}
onLoad={measureHintPosition}
className="max-h-full max-w-full object-contain [-webkit-touch-callout:default]"
/>
</div>