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 />;
|
return <LoadingIndicator />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentImage = images[currentIndex];
|
const currentImage: ILightboxImage | undefined = images[currentIndex];
|
||||||
|
|
||||||
function setRating(v: number | null) {
|
function setRating(v: number | null) {
|
||||||
if (currentImage?.id) {
|
if (currentImage?.id) {
|
||||||
@@ -582,7 +582,7 @@ export const LightboxComponent: React.FC<IProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onIncrementClick() {
|
async function onIncrementClick() {
|
||||||
if (currentImage.id === undefined) return;
|
if (currentImage?.id === undefined) return;
|
||||||
try {
|
try {
|
||||||
await mutateImageIncrementO(currentImage.id);
|
await mutateImageIncrementO(currentImage.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -591,7 +591,7 @@ export const LightboxComponent: React.FC<IProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onDecrementClick() {
|
async function onDecrementClick() {
|
||||||
if (currentImage.id === undefined) return;
|
if (currentImage?.id === undefined) return;
|
||||||
try {
|
try {
|
||||||
await mutateImageDecrementO(currentImage.id);
|
await mutateImageDecrementO(currentImage.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -600,9 +600,9 @@ export const LightboxComponent: React.FC<IProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onResetClick() {
|
async function onResetClick() {
|
||||||
if (currentImage.id === undefined) return;
|
if (currentImage?.id === undefined) return;
|
||||||
try {
|
try {
|
||||||
await mutateImageResetO(currentImage.id);
|
await mutateImageResetO(currentImage?.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
@@ -766,18 +766,18 @@ export const LightboxComponent: React.FC<IProps> = ({
|
|||||||
)}
|
)}
|
||||||
<div className={CLASSNAME_FOOTER}>
|
<div className={CLASSNAME_FOOTER}>
|
||||||
<div className={CLASSNAME_FOOTER_LEFT}>
|
<div className={CLASSNAME_FOOTER_LEFT}>
|
||||||
{currentImage.id !== undefined && (
|
{currentImage?.id !== undefined && (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<OCounterButton
|
<OCounterButton
|
||||||
onDecrement={onDecrementClick}
|
onDecrement={onDecrementClick}
|
||||||
onIncrement={onIncrementClick}
|
onIncrement={onIncrementClick}
|
||||||
onReset={onResetClick}
|
onReset={onResetClick}
|
||||||
value={currentImage.o_counter ?? 0}
|
value={currentImage?.o_counter ?? 0}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<RatingStars
|
<RatingStars
|
||||||
value={currentImage.rating ?? undefined}
|
value={currentImage?.rating ?? undefined}
|
||||||
onSetRating={(v) => {
|
onSetRating={(v) => {
|
||||||
setRating(v ?? null);
|
setRating(v ?? null);
|
||||||
}}
|
}}
|
||||||
@@ -786,7 +786,7 @@ export const LightboxComponent: React.FC<IProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{currentImage.title && (
|
{currentImage?.title && (
|
||||||
<Link to={`/images/${currentImage.id}`} onClick={() => hide()}>
|
<Link to={`/images/${currentImage.id}`} onClick={() => hide()}>
|
||||||
{currentImage.title ?? ""}
|
{currentImage.title ?? ""}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -7,6 +7,44 @@ const CLASSNAME = "Lightbox";
|
|||||||
const CLASSNAME_CAROUSEL = `${CLASSNAME}-carousel`;
|
const CLASSNAME_CAROUSEL = `${CLASSNAME}-carousel`;
|
||||||
const CLASSNAME_IMAGE = `${CLASSNAME_CAROUSEL}-image`;
|
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 {
|
interface IProps {
|
||||||
src: string;
|
src: string;
|
||||||
displayMode: GQL.ImageLightboxDisplayMode;
|
displayMode: GQL.ImageLightboxDisplayMode;
|
||||||
@@ -76,8 +114,58 @@ export const LightboxImage: React.FC<IProps> = ({
|
|||||||
};
|
};
|
||||||
}, [src]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
// don't set anything until we have the heights
|
// don't set anything until we have the dimensions
|
||||||
if (!width || !height || !boxWidth || !boxHeight) {
|
if (!width || !height || !boxWidth || !boxHeight) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -90,80 +178,47 @@ export const LightboxImage: React.FC<IProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// set initial zoom level based on options
|
// set initial zoom level based on options
|
||||||
let xZoom: number;
|
const newZoom = calculateDefaultZoom(
|
||||||
let yZoom: number;
|
width,
|
||||||
let newZoom = 1;
|
height,
|
||||||
let newPositionY = 0;
|
boxWidth,
|
||||||
switch (displayMode) {
|
boxHeight,
|
||||||
case GQL.ImageLightboxDisplayMode.FitXy:
|
displayMode,
|
||||||
xZoom = boxWidth / width;
|
scaleUp
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setDefaultZoom(newZoom);
|
setDefaultZoom(newZoom);
|
||||||
|
|
||||||
|
const [newPositionX, newPositionY] = calculateInitialPosition(newZoom * 1);
|
||||||
|
|
||||||
setPositionX(newPositionX);
|
setPositionX(newPositionX);
|
||||||
setPositionY(newPositionY);
|
setPositionY(newPositionY);
|
||||||
}, [width, height, boxWidth, boxHeight, displayMode, scaleUp, alignBottom]);
|
}, [
|
||||||
|
width,
|
||||||
const calculateInitialPosition = useCallback(() => {
|
height,
|
||||||
// Center image from container's center
|
boxWidth,
|
||||||
const newPositionX = Math.min((boxWidth - width) / 2, 0);
|
boxHeight,
|
||||||
let newPositionY: number;
|
displayMode,
|
||||||
|
scaleUp,
|
||||||
if (zoom * defaultZoom * height > boxHeight) {
|
alignBottom,
|
||||||
if (!alignBottom) {
|
calculateInitialPosition,
|
||||||
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]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (resetPosition !== resetPositionRef.current) {
|
if (resetPosition !== resetPositionRef.current) {
|
||||||
resetPositionRef.current = resetPosition;
|
resetPositionRef.current = resetPosition;
|
||||||
|
|
||||||
const [x, y] = calculateInitialPosition();
|
const [x, y] = calculateInitialPosition(zoom * defaultZoom);
|
||||||
setPositionX(x);
|
setPositionX(x);
|
||||||
setPositionY(y);
|
setPositionY(y);
|
||||||
}
|
}
|
||||||
}, [resetPosition, resetPositionRef, calculateInitialPosition]);
|
}, [
|
||||||
|
zoom,
|
||||||
|
defaultZoom,
|
||||||
|
resetPosition,
|
||||||
|
resetPositionRef,
|
||||||
|
calculateInitialPosition,
|
||||||
|
]);
|
||||||
|
|
||||||
function getScrollMode(ev: React.WheelEvent<HTMLDivElement>) {
|
function getScrollMode(ev: React.WheelEvent<HTMLDivElement>) {
|
||||||
if (ev.shiftKey) {
|
if (ev.shiftKey) {
|
||||||
@@ -186,27 +241,7 @@ export const LightboxImage: React.FC<IProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onImageScrollPanY(ev: React.WheelEvent<HTMLDivElement>) {
|
function onImageScrollPanY(ev: React.WheelEvent<HTMLDivElement>) {
|
||||||
const appliedZoom = zoom * defaultZoom;
|
const [minY, maxY] = minMaxY(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 =
|
let newPositionY =
|
||||||
positionY + (ev.deltaY < 0 ? SCROLL_PAN_STEP : -SCROLL_PAN_STEP);
|
positionY + (ev.deltaY < 0 ? SCROLL_PAN_STEP : -SCROLL_PAN_STEP);
|
||||||
@@ -219,10 +254,8 @@ export const LightboxImage: React.FC<IProps> = ({
|
|||||||
onRight();
|
onRight();
|
||||||
} else {
|
} else {
|
||||||
// ensure image doesn't go offscreen
|
// ensure image doesn't go offscreen
|
||||||
console.log("unconstrained y: " + newPositionY);
|
|
||||||
newPositionY = Math.max(newPositionY, minY);
|
newPositionY = Math.max(newPositionY, minY);
|
||||||
newPositionY = Math.min(newPositionY, maxY);
|
newPositionY = Math.min(newPositionY, maxY);
|
||||||
console.log("positionY: " + positionY + " newPositionY: " + newPositionY);
|
|
||||||
|
|
||||||
setPositionY(newPositionY);
|
setPositionY(newPositionY);
|
||||||
}
|
}
|
||||||
@@ -267,6 +300,8 @@ export const LightboxImage: React.FC<IProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onImageMouseUp(ev: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
function onImageMouseUp(ev: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
||||||
|
if (ev.button !== 0) return;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!mouseDownEvent.current ||
|
!mouseDownEvent.current ||
|
||||||
ev.timeStamp - mouseDownEvent.current.timeStamp > 200
|
ev.timeStamp - mouseDownEvent.current.timeStamp > 200
|
||||||
|
|||||||
Reference in New Issue
Block a user