mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Fix lightbox issues (#2426)
* Don't handle non-left-click events * Improve lightbox initial positioning * Fix crash when navigating left from first image
This commit is contained in:
@@ -566,7 +566,7 @@ export const LightboxComponent: React.FC<IProps> = ({
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
|
||||
const currentImage = images[currentIndex];
|
||||
const currentImage: ILightboxImage | undefined = images[currentIndex];
|
||||
|
||||
function setRating(v: number | null) {
|
||||
if (currentImage?.id) {
|
||||
@@ -582,7 +582,7 @@ export const LightboxComponent: React.FC<IProps> = ({
|
||||
}
|
||||
|
||||
async function onIncrementClick() {
|
||||
if (currentImage.id === undefined) return;
|
||||
if (currentImage?.id === undefined) return;
|
||||
try {
|
||||
await mutateImageIncrementO(currentImage.id);
|
||||
} catch (e) {
|
||||
@@ -591,7 +591,7 @@ export const LightboxComponent: React.FC<IProps> = ({
|
||||
}
|
||||
|
||||
async function onDecrementClick() {
|
||||
if (currentImage.id === undefined) return;
|
||||
if (currentImage?.id === undefined) return;
|
||||
try {
|
||||
await mutateImageDecrementO(currentImage.id);
|
||||
} catch (e) {
|
||||
@@ -600,9 +600,9 @@ export const LightboxComponent: React.FC<IProps> = ({
|
||||
}
|
||||
|
||||
async function onResetClick() {
|
||||
if (currentImage.id === undefined) return;
|
||||
if (currentImage?.id === undefined) return;
|
||||
try {
|
||||
await mutateImageResetO(currentImage.id);
|
||||
await mutateImageResetO(currentImage?.id);
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
@@ -766,18 +766,18 @@ export const LightboxComponent: React.FC<IProps> = ({
|
||||
)}
|
||||
<div className={CLASSNAME_FOOTER}>
|
||||
<div className={CLASSNAME_FOOTER_LEFT}>
|
||||
{currentImage.id !== undefined && (
|
||||
{currentImage?.id !== undefined && (
|
||||
<>
|
||||
<div>
|
||||
<OCounterButton
|
||||
onDecrement={onDecrementClick}
|
||||
onIncrement={onIncrementClick}
|
||||
onReset={onResetClick}
|
||||
value={currentImage.o_counter ?? 0}
|
||||
value={currentImage?.o_counter ?? 0}
|
||||
/>
|
||||
</div>
|
||||
<RatingStars
|
||||
value={currentImage.rating ?? undefined}
|
||||
value={currentImage?.rating ?? undefined}
|
||||
onSetRating={(v) => {
|
||||
setRating(v ?? null);
|
||||
}}
|
||||
@@ -786,7 +786,7 @@ export const LightboxComponent: React.FC<IProps> = ({
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{currentImage.title && (
|
||||
{currentImage?.title && (
|
||||
<Link to={`/images/${currentImage.id}`} onClick={() => hide()}>
|
||||
{currentImage.title ?? ""}
|
||||
</Link>
|
||||
|
||||
@@ -7,6 +7,44 @@ const CLASSNAME = "Lightbox";
|
||||
const CLASSNAME_CAROUSEL = `${CLASSNAME}-carousel`;
|
||||
const CLASSNAME_IMAGE = `${CLASSNAME_CAROUSEL}-image`;
|
||||
|
||||
function calculateDefaultZoom(
|
||||
width: number,
|
||||
height: number,
|
||||
boundWidth: number,
|
||||
boundHeight: number,
|
||||
displayMode: GQL.ImageLightboxDisplayMode,
|
||||
scaleUp: boolean
|
||||
) {
|
||||
// set initial zoom level based on options
|
||||
let xZoom: number;
|
||||
let yZoom: number;
|
||||
let newZoom = 1;
|
||||
switch (displayMode) {
|
||||
case GQL.ImageLightboxDisplayMode.FitXy:
|
||||
xZoom = boundWidth / width;
|
||||
yZoom = boundHeight / height;
|
||||
|
||||
if (!scaleUp) {
|
||||
xZoom = Math.min(xZoom, 1);
|
||||
yZoom = Math.min(yZoom, 1);
|
||||
}
|
||||
newZoom = Math.min(xZoom, yZoom);
|
||||
break;
|
||||
case GQL.ImageLightboxDisplayMode.FitX:
|
||||
newZoom = boundWidth / width;
|
||||
|
||||
if (!scaleUp) {
|
||||
newZoom = Math.min(newZoom, 1);
|
||||
}
|
||||
break;
|
||||
case GQL.ImageLightboxDisplayMode.Original:
|
||||
newZoom = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
return newZoom;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
src: string;
|
||||
displayMode: GQL.ImageLightboxDisplayMode;
|
||||
@@ -76,8 +114,58 @@ export const LightboxImage: React.FC<IProps> = ({
|
||||
};
|
||||
}, [src]);
|
||||
|
||||
const minMaxY = useCallback(
|
||||
(appliedZoom: number) => {
|
||||
let minY, maxY: number;
|
||||
const inBounds = appliedZoom * 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;
|
||||
}
|
||||
|
||||
return [minY, maxY];
|
||||
},
|
||||
[height, boxHeight]
|
||||
);
|
||||
|
||||
const calculateInitialPosition = useCallback(
|
||||
(appliedZoom: number) => {
|
||||
// Center image from container's center
|
||||
const newPositionX = Math.min((boxWidth - width) / 2, 0);
|
||||
let newPositionY: number;
|
||||
|
||||
if (displayMode === GQL.ImageLightboxDisplayMode.FitXy) {
|
||||
newPositionY = Math.min((boxHeight - height) / 2, 0);
|
||||
} else {
|
||||
// otherwise, align image with container
|
||||
const [minY, maxY] = minMaxY(appliedZoom);
|
||||
if (!alignBottom) {
|
||||
newPositionY = maxY;
|
||||
} else {
|
||||
newPositionY = minY;
|
||||
}
|
||||
}
|
||||
|
||||
return [newPositionX, newPositionY];
|
||||
},
|
||||
[displayMode, boxWidth, width, boxHeight, height, alignBottom, minMaxY]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// don't set anything until we have the heights
|
||||
// don't set anything until we have the dimensions
|
||||
if (!width || !height || !boxWidth || !boxHeight) {
|
||||
return;
|
||||
}
|
||||
@@ -90,80 +178,47 @@ export const LightboxImage: React.FC<IProps> = ({
|
||||
}
|
||||
|
||||
// set initial zoom level based on options
|
||||
let xZoom: number;
|
||||
let yZoom: number;
|
||||
let newZoom = 1;
|
||||
let newPositionY = 0;
|
||||
switch (displayMode) {
|
||||
case GQL.ImageLightboxDisplayMode.FitXy:
|
||||
xZoom = boxWidth / width;
|
||||
yZoom = boxHeight / height;
|
||||
|
||||
if (!scaleUp) {
|
||||
xZoom = Math.min(xZoom, 1);
|
||||
yZoom = Math.min(yZoom, 1);
|
||||
}
|
||||
newZoom = Math.min(xZoom, yZoom);
|
||||
break;
|
||||
case GQL.ImageLightboxDisplayMode.FitX:
|
||||
newZoom = boxWidth / width;
|
||||
|
||||
if (!scaleUp) {
|
||||
newZoom = Math.min(newZoom, 1);
|
||||
}
|
||||
break;
|
||||
case GQL.ImageLightboxDisplayMode.Original:
|
||||
newZoom = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
// Center image from container's center
|
||||
const newPositionX = Math.min((boxWidth - width) / 2, 0);
|
||||
|
||||
// if fitting to screen, then centre, other
|
||||
if (displayMode === GQL.ImageLightboxDisplayMode.FitXy) {
|
||||
newPositionY = Math.min((boxHeight - height) / 2, 0);
|
||||
} else {
|
||||
// otherwise, align top of image with container
|
||||
if (!alignBottom) {
|
||||
newPositionY = Math.min((height * newZoom - height) / 2, 0);
|
||||
} else {
|
||||
newPositionY = boxHeight - height * newZoom;
|
||||
}
|
||||
}
|
||||
const newZoom = calculateDefaultZoom(
|
||||
width,
|
||||
height,
|
||||
boxWidth,
|
||||
boxHeight,
|
||||
displayMode,
|
||||
scaleUp
|
||||
);
|
||||
|
||||
setDefaultZoom(newZoom);
|
||||
|
||||
const [newPositionX, newPositionY] = calculateInitialPosition(newZoom * 1);
|
||||
|
||||
setPositionX(newPositionX);
|
||||
setPositionY(newPositionY);
|
||||
}, [width, height, boxWidth, boxHeight, displayMode, scaleUp, alignBottom]);
|
||||
|
||||
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) {
|
||||
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, alignBottom]);
|
||||
}, [
|
||||
width,
|
||||
height,
|
||||
boxWidth,
|
||||
boxHeight,
|
||||
displayMode,
|
||||
scaleUp,
|
||||
alignBottom,
|
||||
calculateInitialPosition,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (resetPosition !== resetPositionRef.current) {
|
||||
resetPositionRef.current = resetPosition;
|
||||
|
||||
const [x, y] = calculateInitialPosition();
|
||||
const [x, y] = calculateInitialPosition(zoom * defaultZoom);
|
||||
setPositionX(x);
|
||||
setPositionY(y);
|
||||
}
|
||||
}, [resetPosition, resetPositionRef, calculateInitialPosition]);
|
||||
}, [
|
||||
zoom,
|
||||
defaultZoom,
|
||||
resetPosition,
|
||||
resetPositionRef,
|
||||
calculateInitialPosition,
|
||||
]);
|
||||
|
||||
function getScrollMode(ev: React.WheelEvent<HTMLDivElement>) {
|
||||
if (ev.shiftKey) {
|
||||
@@ -186,27 +241,7 @@ export const LightboxImage: React.FC<IProps> = ({
|
||||
}
|
||||
|
||||
function onImageScrollPanY(ev: React.WheelEvent<HTMLDivElement>) {
|
||||
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;
|
||||
}
|
||||
const [minY, maxY] = minMaxY(zoom * defaultZoom);
|
||||
|
||||
let newPositionY =
|
||||
positionY + (ev.deltaY < 0 ? SCROLL_PAN_STEP : -SCROLL_PAN_STEP);
|
||||
@@ -219,10 +254,8 @@ export const LightboxImage: React.FC<IProps> = ({
|
||||
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);
|
||||
}
|
||||
@@ -267,6 +300,8 @@ export const LightboxImage: React.FC<IProps> = ({
|
||||
}
|
||||
|
||||
function onImageMouseUp(ev: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
||||
if (ev.button !== 0) return;
|
||||
|
||||
if (
|
||||
!mouseDownEvent.current ||
|
||||
ev.timeStamp - mouseDownEvent.current.timeStamp > 200
|
||||
|
||||
Reference in New Issue
Block a user