mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Prevent lightbox transition until specific number of scroll events (#2544)
* Delay before nav to next image on scroll * Add config for scroll attempts before transition
This commit is contained in:
@@ -69,6 +69,7 @@ fragment ConfigInterfaceData on ConfigInterfaceResult {
|
|||||||
scaleUp
|
scaleUp
|
||||||
resetZoomOnNav
|
resetZoomOnNav
|
||||||
scrollMode
|
scrollMode
|
||||||
|
scrollAttemptsBeforeChange
|
||||||
}
|
}
|
||||||
disableDropdownCreate {
|
disableDropdownCreate {
|
||||||
performer
|
performer
|
||||||
|
|||||||
@@ -217,6 +217,7 @@ input ConfigImageLightboxInput {
|
|||||||
scaleUp: Boolean
|
scaleUp: Boolean
|
||||||
resetZoomOnNav: Boolean
|
resetZoomOnNav: Boolean
|
||||||
scrollMode: ImageLightboxScrollMode
|
scrollMode: ImageLightboxScrollMode
|
||||||
|
scrollAttemptsBeforeChange: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigImageLightboxResult {
|
type ConfigImageLightboxResult {
|
||||||
@@ -225,6 +226,7 @@ type ConfigImageLightboxResult {
|
|||||||
scaleUp: Boolean
|
scaleUp: Boolean
|
||||||
resetZoomOnNav: Boolean
|
resetZoomOnNav: Boolean
|
||||||
scrollMode: ImageLightboxScrollMode
|
scrollMode: ImageLightboxScrollMode
|
||||||
|
scrollAttemptsBeforeChange: Int!
|
||||||
}
|
}
|
||||||
|
|
||||||
input ConfigInterfaceInput {
|
input ConfigInterfaceInput {
|
||||||
|
|||||||
@@ -342,6 +342,10 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.
|
|||||||
setBool(config.ImageLightboxScaleUp, options.ScaleUp)
|
setBool(config.ImageLightboxScaleUp, options.ScaleUp)
|
||||||
setBool(config.ImageLightboxResetZoomOnNav, options.ResetZoomOnNav)
|
setBool(config.ImageLightboxResetZoomOnNav, options.ResetZoomOnNav)
|
||||||
setString(config.ImageLightboxScrollMode, (*string)(options.ScrollMode))
|
setString(config.ImageLightboxScrollMode, (*string)(options.ScrollMode))
|
||||||
|
|
||||||
|
if options.ScrollAttemptsBeforeChange != nil {
|
||||||
|
c.Set(config.ImageLightboxScrollAttemptsBeforeChange, *options.ScrollAttemptsBeforeChange)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.CSS != nil {
|
if input.CSS != nil {
|
||||||
|
|||||||
@@ -145,12 +145,13 @@ const (
|
|||||||
defaultWallPlayback = "video"
|
defaultWallPlayback = "video"
|
||||||
|
|
||||||
// Image lightbox options
|
// Image lightbox options
|
||||||
legacyImageLightboxSlideshowDelay = "slideshow_delay"
|
legacyImageLightboxSlideshowDelay = "slideshow_delay"
|
||||||
ImageLightboxSlideshowDelay = "image_lightbox.slideshow_delay"
|
ImageLightboxSlideshowDelay = "image_lightbox.slideshow_delay"
|
||||||
ImageLightboxDisplayMode = "image_lightbox.display_mode"
|
ImageLightboxDisplayMode = "image_lightbox.display_mode"
|
||||||
ImageLightboxScaleUp = "image_lightbox.scale_up"
|
ImageLightboxScaleUp = "image_lightbox.scale_up"
|
||||||
ImageLightboxResetZoomOnNav = "image_lightbox.reset_zoom_on_nav"
|
ImageLightboxResetZoomOnNav = "image_lightbox.reset_zoom_on_nav"
|
||||||
ImageLightboxScrollMode = "image_lightbox.scroll_mode"
|
ImageLightboxScrollMode = "image_lightbox.scroll_mode"
|
||||||
|
ImageLightboxScrollAttemptsBeforeChange = "image_lightbox.scroll_attempts_before_change"
|
||||||
|
|
||||||
defaultImageLightboxSlideshowDelay = 5000
|
defaultImageLightboxSlideshowDelay = 5000
|
||||||
|
|
||||||
@@ -955,6 +956,9 @@ func (i *Instance) GetImageLightboxOptions() models.ConfigImageLightboxResult {
|
|||||||
mode := models.ImageLightboxScrollMode(v.GetString(ImageLightboxScrollMode))
|
mode := models.ImageLightboxScrollMode(v.GetString(ImageLightboxScrollMode))
|
||||||
ret.ScrollMode = &mode
|
ret.ScrollMode = &mode
|
||||||
}
|
}
|
||||||
|
if v := i.viperWith(ImageLightboxScrollAttemptsBeforeChange); v != nil {
|
||||||
|
ret.ScrollAttemptsBeforeChange = v.GetInt(ImageLightboxScrollAttemptsBeforeChange)
|
||||||
|
}
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
### ✨ New Features
|
### ✨ New Features
|
||||||
* Add support for VTT and SRT captions for scenes. ([#2462](https://github.com/stashapp/stash/pull/2462))
|
* Add support for VTT and SRT captions for scenes. ([#2462](https://github.com/stashapp/stash/pull/2462))
|
||||||
|
* Added option to require a number of scroll attempts before navigating to next/previous image in Lightbox. ([#2544](https://github.com/stashapp/stash/pull/2544))
|
||||||
|
|
||||||
### 🎨 Improvements
|
### 🎨 Improvements
|
||||||
* Changed playback rate options to be the same as those provided by YouTube. ([#2550](https://github.com/stashapp/stash/pull/2550))
|
* Changed playback rate options to be the same as those provided by YouTube. ([#2550](https://github.com/stashapp/stash/pull/2550))
|
||||||
|
|||||||
@@ -289,6 +289,15 @@ export const SettingsInterfacePanel: React.FC = () => {
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</SelectSetting>
|
</SelectSetting>
|
||||||
|
|
||||||
|
<NumberSetting
|
||||||
|
headingID="config.ui.scroll_attempts_before_change.heading"
|
||||||
|
subHeadingID="config.ui.scroll_attempts_before_change.description"
|
||||||
|
value={iface.imageLightbox?.scrollAttemptsBeforeChange ?? 0}
|
||||||
|
onChange={(v) =>
|
||||||
|
saveLightboxSettings({ scrollAttemptsBeforeChange: v })
|
||||||
|
}
|
||||||
|
/>
|
||||||
</SettingSection>
|
</SettingSection>
|
||||||
|
|
||||||
<SettingSection headingID="config.ui.editing.heading">
|
<SettingSection headingID="config.ui.editing.heading">
|
||||||
|
|||||||
@@ -158,6 +158,11 @@ export const LightboxComponent: React.FC<IProps> = ({
|
|||||||
const slideshowDelay =
|
const slideshowDelay =
|
||||||
savedDelay ?? configuredDelay ?? DEFAULT_SLIDESHOW_DELAY;
|
savedDelay ?? configuredDelay ?? DEFAULT_SLIDESHOW_DELAY;
|
||||||
|
|
||||||
|
const scrollAttemptsBeforeChange = Math.max(
|
||||||
|
0,
|
||||||
|
config?.interface.imageLightbox.scrollAttemptsBeforeChange ?? 0
|
||||||
|
);
|
||||||
|
|
||||||
function setSlideshowDelay(v: number) {
|
function setSlideshowDelay(v: number) {
|
||||||
setLightboxSettings({ slideshowDelay: v });
|
setLightboxSettings({ slideshowDelay: v });
|
||||||
}
|
}
|
||||||
@@ -733,6 +738,8 @@ export const LightboxComponent: React.FC<IProps> = ({
|
|||||||
onRight={handleRight}
|
onRight={handleRight}
|
||||||
alignBottom={movingLeft}
|
alignBottom={movingLeft}
|
||||||
zoom={i === currentIndex ? zoom : 1}
|
zoom={i === currentIndex ? zoom : 1}
|
||||||
|
current={i === currentIndex}
|
||||||
|
scrollAttemptsBeforeChange={scrollAttemptsBeforeChange}
|
||||||
setZoom={(v) => setZoom(v)}
|
setZoom={(v) => setZoom(v)}
|
||||||
resetPosition={resetPosition}
|
resetPosition={resetPosition}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ interface IProps {
|
|||||||
scrollMode: GQL.ImageLightboxScrollMode;
|
scrollMode: GQL.ImageLightboxScrollMode;
|
||||||
resetPosition?: boolean;
|
resetPosition?: boolean;
|
||||||
zoom: number;
|
zoom: number;
|
||||||
|
scrollAttemptsBeforeChange: number;
|
||||||
|
current: boolean;
|
||||||
// set to true to align image with bottom instead of top
|
// set to true to align image with bottom instead of top
|
||||||
alignBottom?: boolean;
|
alignBottom?: boolean;
|
||||||
setZoom: (v: number) => void;
|
setZoom: (v: number) => void;
|
||||||
@@ -68,6 +70,8 @@ export const LightboxImage: React.FC<IProps> = ({
|
|||||||
scrollMode,
|
scrollMode,
|
||||||
alignBottom,
|
alignBottom,
|
||||||
zoom,
|
zoom,
|
||||||
|
scrollAttemptsBeforeChange,
|
||||||
|
current,
|
||||||
setZoom,
|
setZoom,
|
||||||
resetPosition,
|
resetPosition,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -88,6 +92,8 @@ export const LightboxImage: React.FC<IProps> = ({
|
|||||||
const pointerCache = useRef<React.PointerEvent<HTMLDivElement>[]>([]);
|
const pointerCache = useRef<React.PointerEvent<HTMLDivElement>[]>([]);
|
||||||
const prevDiff = useRef<number | undefined>();
|
const prevDiff = useRef<number | undefined>();
|
||||||
|
|
||||||
|
const scrollAttempts = useRef(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const box = container.current;
|
const box = container.current;
|
||||||
if (box) {
|
if (box) {
|
||||||
@@ -193,6 +199,12 @@ export const LightboxImage: React.FC<IProps> = ({
|
|||||||
|
|
||||||
setPositionX(newPositionX);
|
setPositionX(newPositionX);
|
||||||
setPositionY(newPositionY);
|
setPositionY(newPositionY);
|
||||||
|
|
||||||
|
if (alignBottom) {
|
||||||
|
scrollAttempts.current = scrollAttemptsBeforeChange;
|
||||||
|
} else {
|
||||||
|
scrollAttempts.current = -scrollAttemptsBeforeChange;
|
||||||
|
}
|
||||||
}, [
|
}, [
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
@@ -202,6 +214,7 @@ export const LightboxImage: React.FC<IProps> = ({
|
|||||||
scaleUp,
|
scaleUp,
|
||||||
alignBottom,
|
alignBottom,
|
||||||
calculateInitialPosition,
|
calculateInitialPosition,
|
||||||
|
scrollAttemptsBeforeChange,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -241,26 +254,48 @@ export const LightboxImage: React.FC<IProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onImageScrollPanY(ev: React.WheelEvent<HTMLDivElement>) {
|
function onImageScrollPanY(ev: React.WheelEvent<HTMLDivElement>) {
|
||||||
const [minY, maxY] = minMaxY(zoom * defaultZoom);
|
if (current) {
|
||||||
|
const [minY, maxY] = minMaxY(zoom * defaultZoom);
|
||||||
|
|
||||||
let newPositionY =
|
const scrollable = positionY !== maxY || positionY !== minY;
|
||||||
positionY + (ev.deltaY < 0 ? SCROLL_PAN_STEP : -SCROLL_PAN_STEP);
|
|
||||||
|
|
||||||
// #2389 - if scroll up and at top, then go to previous image
|
let newPositionY =
|
||||||
// if scroll down and at bottom, then go to next image
|
positionY + (ev.deltaY < 0 ? SCROLL_PAN_STEP : -SCROLL_PAN_STEP);
|
||||||
if (newPositionY > maxY && positionY === maxY) {
|
|
||||||
onLeft();
|
|
||||||
} else if (newPositionY < minY && positionY === minY) {
|
|
||||||
onRight();
|
|
||||||
} else {
|
|
||||||
// ensure image doesn't go offscreen
|
|
||||||
newPositionY = Math.max(newPositionY, minY);
|
|
||||||
newPositionY = Math.min(newPositionY, maxY);
|
|
||||||
|
|
||||||
setPositionY(newPositionY);
|
// #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) {
|
||||||
|
// #2535 - require additional scrolls before changing page
|
||||||
|
if (
|
||||||
|
!scrollable ||
|
||||||
|
scrollAttempts.current <= -scrollAttemptsBeforeChange
|
||||||
|
) {
|
||||||
|
onLeft();
|
||||||
|
} else {
|
||||||
|
scrollAttempts.current--;
|
||||||
|
}
|
||||||
|
} else if (newPositionY < minY && positionY === minY) {
|
||||||
|
// #2535 - require additional scrolls before changing page
|
||||||
|
if (
|
||||||
|
!scrollable ||
|
||||||
|
scrollAttempts.current >= scrollAttemptsBeforeChange
|
||||||
|
) {
|
||||||
|
onRight();
|
||||||
|
} else {
|
||||||
|
scrollAttempts.current++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
scrollAttempts.current = 0;
|
||||||
|
|
||||||
|
// ensure image doesn't go offscreen
|
||||||
|
newPositionY = Math.max(newPositionY, minY);
|
||||||
|
newPositionY = Math.min(newPositionY, maxY);
|
||||||
|
|
||||||
|
setPositionY(newPositionY);
|
||||||
|
}
|
||||||
|
|
||||||
|
ev.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
ev.stopPropagation();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onImageScroll(ev: React.WheelEvent<HTMLDivElement>) {
|
function onImageScroll(ev: React.WheelEvent<HTMLDivElement>) {
|
||||||
@@ -418,7 +453,7 @@ export const LightboxImage: React.FC<IProps> = ({
|
|||||||
alt=""
|
alt=""
|
||||||
draggable={false}
|
draggable={false}
|
||||||
style={{ touchAction: "none" }}
|
style={{ touchAction: "none" }}
|
||||||
onWheel={(e) => onImageScroll(e)}
|
onWheel={current ? (e) => onImageScroll(e) : undefined}
|
||||||
onMouseDown={(e) => onImageMouseDown(e)}
|
onMouseDown={(e) => onImageMouseDown(e)}
|
||||||
onMouseUp={(e) => onImageMouseUp(e)}
|
onMouseUp={(e) => onImageMouseUp(e)}
|
||||||
onMouseMove={(e) => onImageMouseOver(e)}
|
onMouseMove={(e) => onImageMouseOver(e)}
|
||||||
|
|||||||
@@ -522,6 +522,10 @@
|
|||||||
"toggle_sound": "Enable sound"
|
"toggle_sound": "Enable sound"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"scroll_attempts_before_change": {
|
||||||
|
"description": "Number of times to attempt to scroll before moving to the next/previous item. Only applies for Pan Y scroll mode.",
|
||||||
|
"heading": "Scroll attempts before transition"
|
||||||
|
},
|
||||||
"slideshow_delay": {
|
"slideshow_delay": {
|
||||||
"description": "Slideshow is available in galleries when in wall view mode",
|
"description": "Slideshow is available in galleries when in wall view mode",
|
||||||
"heading": "Slideshow Delay (seconds)"
|
"heading": "Slideshow Delay (seconds)"
|
||||||
|
|||||||
Reference in New Issue
Block a user