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:
WithoutPants
2022-05-06 12:22:26 +10:00
committed by GitHub
parent c1a096a1a6
commit 73ded0d97d
9 changed files with 90 additions and 23 deletions

View File

@@ -69,6 +69,7 @@ fragment ConfigInterfaceData on ConfigInterfaceResult {
scaleUp scaleUp
resetZoomOnNav resetZoomOnNav
scrollMode scrollMode
scrollAttemptsBeforeChange
} }
disableDropdownCreate { disableDropdownCreate {
performer performer

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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
} }

View File

@@ -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))

View File

@@ -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">

View File

@@ -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}
/> />

View File

@@ -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)}

View File

@@ -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)"