Scroll to next image using lightbox (#2403)

* Scroll at end of image goes to next/previous
* Align bottom image when moving left
This commit is contained in:
WithoutPants
2022-03-22 11:00:32 +11:00
committed by GitHub
parent 5eee305a33
commit 228e8c9bfd
3 changed files with 71 additions and 17 deletions

View File

@@ -1,4 +1,5 @@
### 🎨 Improvements ### 🎨 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)) * 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)) * Improved autotag performance. ([#2368](https://github.com/stashapp/stash/pull/2368))

View File

@@ -89,6 +89,7 @@ export const LightboxComponent: React.FC<IProps> = ({
const [updateImage] = useImageUpdate(); const [updateImage] = useImageUpdate();
const [index, setIndex] = useState<number | null>(null); const [index, setIndex] = useState<number | null>(null);
const [movingLeft, setMovingLeft] = useState(false);
const oldIndex = useRef<number | null>(null); const oldIndex = useRef<number | null>(null);
const [instantTransition, setInstantTransition] = useState(false); const [instantTransition, setInstantTransition] = useState(false);
const [isSwitchingPage, setIsSwitchingPage] = useState(true); const [isSwitchingPage, setIsSwitchingPage] = useState(true);
@@ -261,6 +262,8 @@ export const LightboxComponent: React.FC<IProps> = ({
(isUserAction = true) => { (isUserAction = true) => {
if (isSwitchingPage || index === -1) return; if (isSwitchingPage || index === -1) return;
setMovingLeft(true);
if (index === 0) { if (index === 0) {
// go to next page, or loop back if no callback is set // go to next page, or loop back if no callback is set
if (pageCallback) { if (pageCallback) {
@@ -281,6 +284,8 @@ export const LightboxComponent: React.FC<IProps> = ({
(isUserAction = true) => { (isUserAction = true) => {
if (isSwitchingPage) return; if (isSwitchingPage) return;
setMovingLeft(false);
if (index === images.length - 1) { if (index === images.length - 1) {
// go to preview page, or loop back if no callback is set // go to preview page, or loop back if no callback is set
if (pageCallback) { if (pageCallback) {
@@ -685,6 +690,7 @@ export const LightboxComponent: React.FC<IProps> = ({
scrollMode={scrollMode} scrollMode={scrollMode}
onLeft={handleLeft} onLeft={handleLeft}
onRight={handleRight} onRight={handleRight}
alignBottom={movingLeft}
zoom={i === currentIndex ? zoom : 1} zoom={i === currentIndex ? zoom : 1}
setZoom={(v) => setZoom(v)} setZoom={(v) => setZoom(v)}
resetPosition={resetPosition} resetPosition={resetPosition}

View File

@@ -24,6 +24,8 @@ interface IProps {
scrollMode: ScrollMode; scrollMode: ScrollMode;
resetPosition?: boolean; resetPosition?: boolean;
zoom: number; zoom: number;
// set to true to align image with bottom instead of top
alignBottom?: boolean;
setZoom: (v: number) => void; setZoom: (v: number) => void;
onLeft: () => void; onLeft: () => void;
onRight: () => void; onRight: () => void;
@@ -36,6 +38,7 @@ export const LightboxImage: React.FC<IProps> = ({
displayMode, displayMode,
scaleUp, scaleUp,
scrollMode, scrollMode,
alignBottom,
zoom, zoom,
setZoom, setZoom,
resetPosition, resetPosition,
@@ -132,37 +135,45 @@ export const LightboxImage: React.FC<IProps> = ({
newPositionY = Math.min((boxHeight - height) / 2, 0); newPositionY = Math.min((boxHeight - height) / 2, 0);
} else { } else {
// otherwise, align top of image with container // otherwise, align top of image with container
if (!alignBottom) {
newPositionY = Math.min((height * newZoom - height) / 2, 0); newPositionY = Math.min((height * newZoom - height) / 2, 0);
} else {
newPositionY = boxHeight - height * newZoom;
}
} }
setDefaultZoom(newZoom); setDefaultZoom(newZoom);
setPositionX(newPositionX); setPositionX(newPositionX);
setPositionY(newPositionY); 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 // Center image from container's center
const newPositionX = Math.min((boxWidth - width) / 2, 0); const newPositionX = Math.min((boxWidth - width) / 2, 0);
let newPositionY: number; let newPositionY: number;
if (zoom * defaultZoom * height > boxHeight) { if (zoom * defaultZoom * height > boxHeight) {
if (!alignBottom) {
newPositionY = (height * zoom * defaultZoom - height) / 2; newPositionY = (height * zoom * defaultZoom - height) / 2;
} else {
newPositionY = boxHeight - height * zoom * defaultZoom;
}
} else { } else {
newPositionY = Math.min((boxHeight - height) / 2, 0); newPositionY = Math.min((boxHeight - height) / 2, 0);
} }
return [newPositionX, newPositionY]; return [newPositionX, newPositionY];
}, [boxWidth, width, boxHeight, height, zoom, defaultZoom]); }, [boxWidth, width, boxHeight, height, zoom, defaultZoom, alignBottom]);
useEffect(() => { useEffect(() => {
if (resetPosition !== resetPositionRef.current) { if (resetPosition !== resetPositionRef.current) {
resetPositionRef.current = resetPosition; resetPositionRef.current = resetPosition;
const [x, y] = calculateTopPosition(); const [x, y] = calculateInitialPosition();
setPositionX(x); setPositionX(x);
setPositionY(y); setPositionY(y);
} }
}, [resetPosition, resetPositionRef, calculateTopPosition]); }, [resetPosition, resetPositionRef, calculateInitialPosition]);
function getScrollMode(ev: React.WheelEvent<HTMLDivElement>) { function getScrollMode(ev: React.WheelEvent<HTMLDivElement>) {
if (ev.shiftKey) { if (ev.shiftKey) {
@@ -184,24 +195,60 @@ export const LightboxImage: React.FC<IProps> = ({
} }
} }
function onImageScroll(ev: React.WheelEvent<HTMLDivElement>) { function onImageScrollPanY(ev: React.WheelEvent<HTMLDivElement>) {
const percent = ev.deltaY < 0 ? ZOOM_STEP : 1 / ZOOM_STEP; const appliedZoom = zoom * defaultZoom;
const minY = (defaultZoom * height - height) / 2 - defaultZoom * height + 1;
const maxY = (defaultZoom * height - height) / 2 + boxHeight - 1; 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 = let newPositionY =
positionY + (ev.deltaY < 0 ? SCROLL_PAN_STEP : -SCROLL_PAN_STEP); 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<HTMLDivElement>) {
const percent = ev.deltaY < 0 ? ZOOM_STEP : 1 / ZOOM_STEP;
switch (getScrollMode(ev)) { switch (getScrollMode(ev)) {
case ScrollMode.ZOOM: case ScrollMode.ZOOM:
setZoom(zoom * percent); setZoom(zoom * percent);
break; break;
case ScrollMode.PAN_Y: case ScrollMode.PAN_Y:
// ensure image doesn't go offscreen onImageScrollPanY(ev);
newPositionY = Math.max(newPositionY, minY);
newPositionY = Math.min(newPositionY, maxY);
setPositionY(newPositionY);
ev.stopPropagation();
break; break;
} }
} }