From 228e8c9bfdae342ea19974d4f376bde89e080bb2 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Tue, 22 Mar 2022 11:00:32 +1100 Subject: [PATCH] Scroll to next image using lightbox (#2403) * Scroll at end of image goes to next/previous * Align bottom image when moving left --- .../components/Changelog/versions/v0140.md | 1 + ui/v2.5/src/hooks/Lightbox/Lightbox.tsx | 6 ++ ui/v2.5/src/hooks/Lightbox/LightboxImage.tsx | 81 +++++++++++++++---- 3 files changed, 71 insertions(+), 17 deletions(-) diff --git a/ui/v2.5/src/components/Changelog/versions/v0140.md b/ui/v2.5/src/components/Changelog/versions/v0140.md index 7ac520263..81c9b4a26 100644 --- a/ui/v2.5/src/components/Changelog/versions/v0140.md +++ b/ui/v2.5/src/components/Changelog/versions/v0140.md @@ -1,4 +1,5 @@ ### 🎨 Improvements +* Image lightbox now transitions to next/previous image when scrolling in pan-Y mode. ([#2403](https://github.com/stashapp/stash/pull/2403)) * Allow customisation of UI theme color using `theme_color` property in `config.yml` ([#2365](https://github.com/stashapp/stash/pull/2365)) * Improved autotag performance. ([#2368](https://github.com/stashapp/stash/pull/2368)) diff --git a/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx b/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx index 2a74e8541..078b80b23 100644 --- a/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx +++ b/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx @@ -89,6 +89,7 @@ export const LightboxComponent: React.FC = ({ const [updateImage] = useImageUpdate(); const [index, setIndex] = useState(null); + const [movingLeft, setMovingLeft] = useState(false); const oldIndex = useRef(null); const [instantTransition, setInstantTransition] = useState(false); const [isSwitchingPage, setIsSwitchingPage] = useState(true); @@ -261,6 +262,8 @@ export const LightboxComponent: React.FC = ({ (isUserAction = true) => { if (isSwitchingPage || index === -1) return; + setMovingLeft(true); + if (index === 0) { // go to next page, or loop back if no callback is set if (pageCallback) { @@ -281,6 +284,8 @@ export const LightboxComponent: React.FC = ({ (isUserAction = true) => { if (isSwitchingPage) return; + setMovingLeft(false); + if (index === images.length - 1) { // go to preview page, or loop back if no callback is set if (pageCallback) { @@ -685,6 +690,7 @@ export const LightboxComponent: React.FC = ({ scrollMode={scrollMode} onLeft={handleLeft} onRight={handleRight} + alignBottom={movingLeft} zoom={i === currentIndex ? zoom : 1} setZoom={(v) => setZoom(v)} resetPosition={resetPosition} diff --git a/ui/v2.5/src/hooks/Lightbox/LightboxImage.tsx b/ui/v2.5/src/hooks/Lightbox/LightboxImage.tsx index 4b468ec7c..a4e4f554f 100644 --- a/ui/v2.5/src/hooks/Lightbox/LightboxImage.tsx +++ b/ui/v2.5/src/hooks/Lightbox/LightboxImage.tsx @@ -24,6 +24,8 @@ interface IProps { scrollMode: ScrollMode; resetPosition?: boolean; zoom: number; + // set to true to align image with bottom instead of top + alignBottom?: boolean; setZoom: (v: number) => void; onLeft: () => void; onRight: () => void; @@ -36,6 +38,7 @@ export const LightboxImage: React.FC = ({ displayMode, scaleUp, scrollMode, + alignBottom, zoom, setZoom, resetPosition, @@ -132,37 +135,45 @@ export const LightboxImage: React.FC = ({ newPositionY = Math.min((boxHeight - height) / 2, 0); } else { // otherwise, align top of image with container - newPositionY = Math.min((height * newZoom - height) / 2, 0); + if (!alignBottom) { + newPositionY = Math.min((height * newZoom - height) / 2, 0); + } else { + newPositionY = boxHeight - height * newZoom; + } } setDefaultZoom(newZoom); setPositionX(newPositionX); setPositionY(newPositionY); - }, [width, height, boxWidth, boxHeight, displayMode, scaleUp]); + }, [width, height, boxWidth, boxHeight, displayMode, scaleUp, alignBottom]); - const calculateTopPosition = useCallback(() => { + const calculateInitialPosition = useCallback(() => { // Center image from container's center const newPositionX = Math.min((boxWidth - width) / 2, 0); let newPositionY: number; if (zoom * defaultZoom * height > boxHeight) { - newPositionY = (height * zoom * defaultZoom - height) / 2; + if (!alignBottom) { + newPositionY = (height * zoom * defaultZoom - height) / 2; + } else { + newPositionY = boxHeight - height * zoom * defaultZoom; + } } else { newPositionY = Math.min((boxHeight - height) / 2, 0); } return [newPositionX, newPositionY]; - }, [boxWidth, width, boxHeight, height, zoom, defaultZoom]); + }, [boxWidth, width, boxHeight, height, zoom, defaultZoom, alignBottom]); useEffect(() => { if (resetPosition !== resetPositionRef.current) { resetPositionRef.current = resetPosition; - const [x, y] = calculateTopPosition(); + const [x, y] = calculateInitialPosition(); setPositionX(x); setPositionY(y); } - }, [resetPosition, resetPositionRef, calculateTopPosition]); + }, [resetPosition, resetPositionRef, calculateInitialPosition]); function getScrollMode(ev: React.WheelEvent) { if (ev.shiftKey) { @@ -184,24 +195,60 @@ export const LightboxImage: React.FC = ({ } } - function onImageScroll(ev: React.WheelEvent) { - const percent = ev.deltaY < 0 ? ZOOM_STEP : 1 / ZOOM_STEP; - const minY = (defaultZoom * height - height) / 2 - defaultZoom * height + 1; - const maxY = (defaultZoom * height - height) / 2 + boxHeight - 1; + function onImageScrollPanY(ev: React.WheelEvent) { + const appliedZoom = zoom * defaultZoom; + + let minY, maxY: number; + const inBounds = zoom * defaultZoom * height <= boxHeight; + + // NOTE: I don't even know how these work, but they do + if (!inBounds) { + if (height > boxHeight) { + minY = + (appliedZoom * height - height) / 2 - + appliedZoom * height + + boxHeight; + maxY = (appliedZoom * height - height) / 2; + } else { + minY = (boxHeight - appliedZoom * height) / 2; + maxY = (appliedZoom * height - boxHeight) / 2; + } + } else { + minY = Math.min((boxHeight - height) / 2, 0); + maxY = minY; + } + let newPositionY = positionY + (ev.deltaY < 0 ? SCROLL_PAN_STEP : -SCROLL_PAN_STEP); + // #2389 - if scroll up and at top, then go to previous image + // if scroll down and at bottom, then go to next image + if (newPositionY > maxY && positionY === maxY) { + onLeft(); + } else if (newPositionY < minY && positionY === minY) { + onRight(); + } else { + // ensure image doesn't go offscreen + console.log("unconstrained y: " + newPositionY); + newPositionY = Math.max(newPositionY, minY); + newPositionY = Math.min(newPositionY, maxY); + console.log("positionY: " + positionY + " newPositionY: " + newPositionY); + + setPositionY(newPositionY); + } + + ev.stopPropagation(); + } + + function onImageScroll(ev: React.WheelEvent) { + const percent = ev.deltaY < 0 ? ZOOM_STEP : 1 / ZOOM_STEP; + switch (getScrollMode(ev)) { case ScrollMode.ZOOM: setZoom(zoom * percent); break; case ScrollMode.PAN_Y: - // ensure image doesn't go offscreen - newPositionY = Math.max(newPositionY, minY); - newPositionY = Math.min(newPositionY, maxY); - - setPositionY(newPositionY); - ev.stopPropagation(); + onImageScrollPanY(ev); break; } }