mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
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:
@@ -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))
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user