Persist lightbox settings (#2406)

* Persist lightbox settings in local forage
* Add lightbox settings to backend
* Add lightbox settings to interface settings page
This commit is contained in:
WithoutPants 2022-03-23 08:18:12 +11:00 committed by GitHub
parent 4c4cdae1ed
commit 2afb467bb1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 382 additions and 114 deletions

View file

@ -62,7 +62,13 @@ fragment ConfigInterfaceData on ConfigInterfaceResult {
css
cssEnabled
language
slideshowDelay
imageLightbox {
slideshowDelay
displayMode
scaleUp
resetZoomOnNav
scrollMode
}
disableDropdownCreate {
performer
tag

View file

@ -196,6 +196,33 @@ input ConfigDisableDropdownCreateInput {
studio: Boolean
}
enum ImageLightboxDisplayMode {
ORIGINAL
FIT_XY
FIT_X
}
enum ImageLightboxScrollMode {
ZOOM
PAN_Y
}
input ConfigImageLightboxInput {
slideshowDelay: Int
displayMode: ImageLightboxDisplayMode
scaleUp: Boolean
resetZoomOnNav: Boolean
scrollMode: ImageLightboxScrollMode
}
type ConfigImageLightboxResult {
slideshowDelay: Int
displayMode: ImageLightboxDisplayMode
scaleUp: Boolean
resetZoomOnNav: Boolean
scrollMode: ImageLightboxScrollMode
}
input ConfigInterfaceInput {
"""Ordered list of items that should be shown in the menu"""
menuItems: [String!]
@ -229,9 +256,11 @@ input ConfigInterfaceInput {
"""Interface language"""
language: String
"""Slideshow Delay"""
slideshowDelay: Int
slideshowDelay: Int @deprecated(reason: "Use imageLightbox.slideshowDelay")
imageLightbox: ConfigImageLightboxInput
"""Set to true to disable creating new objects via the dropdown menus"""
disableDropdownCreate: ConfigDisableDropdownCreateInput
@ -291,7 +320,9 @@ type ConfigInterfaceResult {
language: String
"""Slideshow Delay"""
slideshowDelay: Int
slideshowDelay: Int @deprecated(reason: "Use imageLightbox.slideshowDelay")
imageLightbox: ConfigImageLightboxResult!
"""Fields are true if creating via dropdown menus are disabled"""
disableDropdownCreate: ConfigDisableDropdownCreate!

View file

@ -286,6 +286,12 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.
}
}
setString := func(key string, v *string) {
if v != nil {
c.Set(key, *v)
}
}
if input.MenuItems != nil {
c.Set(config.MenuItems, input.MenuItems)
}
@ -316,8 +322,22 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.
c.Set(config.Language, *input.Language)
}
// deprecated field
if input.SlideshowDelay != nil {
c.Set(config.SlideshowDelay, *input.SlideshowDelay)
c.Set(config.ImageLightboxSlideshowDelay, *input.SlideshowDelay)
}
if input.ImageLightbox != nil {
options := input.ImageLightbox
if options.SlideshowDelay != nil {
c.Set(config.ImageLightboxSlideshowDelay, *options.SlideshowDelay)
}
setString(config.ImageLightboxDisplayMode, (*string)(options.DisplayMode))
setBool(config.ImageLightboxScaleUp, options.ScaleUp)
setBool(config.ImageLightboxResetZoomOnNav, options.ResetZoomOnNav)
setString(config.ImageLightboxScrollMode, (*string)(options.ScrollMode))
}
if input.CSS != nil {

View file

@ -140,9 +140,9 @@ func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
css := config.GetCSS()
cssEnabled := config.GetCSSEnabled()
language := config.GetLanguage()
slideshowDelay := config.GetSlideshowDelay()
handyKey := config.GetHandyKey()
scriptOffset := config.GetFunscriptOffset()
imageLightboxOptions := config.GetImageLightboxOptions()
// FIXME - misnamed output field means we have redundant fields
disableDropdownCreate := config.GetDisableDropdownCreate()
@ -163,7 +163,8 @@ func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
CSS: &css,
CSSEnabled: &cssEnabled,
Language: &language,
SlideshowDelay: &slideshowDelay,
ImageLightbox: &imageLightboxOptions,
// FIXME - see above
DisabledDropdownCreate: disableDropdownCreate,

View file

@ -142,8 +142,15 @@ const (
WallPlayback = "wall_playback"
defaultWallPlayback = "video"
SlideshowDelay = "slideshow_delay"
defaultSlideshowDelay = 5000
// Image lightbox options
legacyImageLightboxSlideshowDelay = "slideshow_delay"
ImageLightboxSlideshowDelay = "image_lightbox.slideshow_delay"
ImageLightboxDisplayMode = "image_lightbox.display_mode"
ImageLightboxScaleUp = "image_lightbox.scale_up"
ImageLightboxResetZoomOnNav = "image_lightbox.reset_zoom_on_nav"
ImageLightboxScrollMode = "image_lightbox.scroll_mode"
defaultImageLightboxSlideshowDelay = 5000
DisableDropdownCreatePerformer = "disable_dropdown_create.performer"
DisableDropdownCreateStudio = "disable_dropdown_create.studio"
@ -364,6 +371,18 @@ func (i *Instance) viper(key string) *viper.Viper {
return v
}
// viper returns the viper instance that has the key set. Returns nil
// if no instance has the key. Assumes read lock held.
func (i *Instance) viperWith(key string) *viper.Viper {
v := i.viper(key)
if v.IsSet(key) {
return v
}
return nil
}
func (i *Instance) HasOverride(key string) bool {
i.RLock()
defer i.RUnlock()
@ -886,14 +905,49 @@ func (i *Instance) GetShowStudioAsText() bool {
return i.getBool(ShowStudioAsText)
}
func (i *Instance) GetSlideshowDelay() int {
func (i *Instance) getSlideshowDelay() int {
// assume have lock
ret := defaultImageLightboxSlideshowDelay
v := i.viper(ImageLightboxSlideshowDelay)
if v.IsSet(ImageLightboxSlideshowDelay) {
ret = v.GetInt(ImageLightboxSlideshowDelay)
} else {
// fallback to old location
v := i.viper(legacyImageLightboxSlideshowDelay)
if v.IsSet(legacyImageLightboxSlideshowDelay) {
ret = v.GetInt(legacyImageLightboxSlideshowDelay)
}
}
return ret
}
func (i *Instance) GetImageLightboxOptions() models.ConfigImageLightboxResult {
i.RLock()
defer i.RUnlock()
ret := defaultSlideshowDelay
v := i.viper(SlideshowDelay)
if v.IsSet(SlideshowDelay) {
ret = v.GetInt(SlideshowDelay)
delay := i.getSlideshowDelay()
ret := models.ConfigImageLightboxResult{
SlideshowDelay: &delay,
}
if v := i.viperWith(ImageLightboxDisplayMode); v != nil {
mode := models.ImageLightboxDisplayMode(v.GetString(ImageLightboxDisplayMode))
ret.DisplayMode = &mode
}
if v := i.viperWith(ImageLightboxScaleUp); v != nil {
value := v.GetBool(ImageLightboxScaleUp)
ret.ScaleUp = &value
}
if v := i.viperWith(ImageLightboxResetZoomOnNav); v != nil {
value := v.GetBool(ImageLightboxResetZoomOnNav)
ret.ResetZoomOnNav = &value
}
if v := i.viperWith(ImageLightboxScrollMode); v != nil {
mode := models.ImageLightboxScrollMode(v.GetString(ImageLightboxScrollMode))
ret.ScrollMode = &mode
}
return ret

View file

@ -81,7 +81,8 @@ func TestConcurrentConfigAccess(t *testing.T) {
i.Set(MaximumLoopDuration, i.GetMaximumLoopDuration())
i.Set(AutostartVideo, i.GetAutostartVideo())
i.Set(ShowStudioAsText, i.GetShowStudioAsText())
i.Set(SlideshowDelay, i.GetSlideshowDelay())
i.Set(legacyImageLightboxSlideshowDelay, *i.GetImageLightboxOptions().SlideshowDelay)
i.Set(ImageLightboxSlideshowDelay, *i.GetImageLightboxOptions().SlideshowDelay)
i.GetCSSPath()
i.GetCSS()
i.Set(CSSEnabled, i.GetCSSEnabled())

View file

@ -1,4 +1,5 @@
### 🎨 Improvements
* Maintain lightbox settings and add lightbox settings to Interface settings page. ([#2406](https://github.com/stashapp/stash/pull/2406))
* 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))
* Improved autotag performance. ([#2368](https://github.com/stashapp/stash/pull/2368))

View file

@ -13,6 +13,12 @@ import {
} from "../Inputs";
import { SettingStateContext } from "../context";
import { DurationUtils } from "src/utils";
import * as GQL from "src/core/generated-graphql";
import {
imageLightboxDisplayModeIntlMap,
imageLightboxScrollModeIntlMap,
} from "src/core/enums";
import { useInterfaceLocalForage } from "src/hooks";
const allMenuItems = [
{ id: "scenes", headingID: "scenes" },
@ -32,6 +38,28 @@ export const SettingsInterfacePanel: React.FC = () => {
SettingStateContext
);
const [, setInterfaceLocalForage] = useInterfaceLocalForage();
function saveLightboxSettings(v: Partial<GQL.ConfigImageLightboxInput>) {
// save in local forage as well for consistency
setInterfaceLocalForage((prev) => {
return {
...prev,
imageLightbox: {
...prev.imageLightbox,
...v,
},
};
});
saveInterface({
imageLightbox: {
...iface.imageLightbox,
...v,
},
});
}
if (error) return <h1>{error.message}</h1>;
if (loading) return <LoadingIndicator />;
@ -195,13 +223,72 @@ export const SettingsInterfacePanel: React.FC = () => {
/>
</SettingSection>
<SettingSection headingID="config.ui.images.heading">
<SettingSection headingID="config.ui.image_lightbox.heading">
<NumberSetting
headingID="config.ui.slideshow_delay.heading"
subHeadingID="config.ui.slideshow_delay.description"
value={iface.slideshowDelay ?? undefined}
onChange={(v) => saveInterface({ slideshowDelay: v })}
value={iface.imageLightbox?.slideshowDelay ?? undefined}
onChange={(v) => saveLightboxSettings({ slideshowDelay: v })}
/>
<SelectSetting
id="lightbox_display_mode"
headingID="dialogs.lightbox.display_mode.label"
value={
iface.imageLightbox?.displayMode ??
GQL.ImageLightboxDisplayMode.FitXy
}
onChange={(v) =>
saveLightboxSettings({
displayMode: v as GQL.ImageLightboxDisplayMode,
})
}
>
{Array.from(imageLightboxDisplayModeIntlMap.entries()).map((v) => (
<option key={v[0]} value={v[0]}>
{intl.formatMessage({
id: v[1],
})}
</option>
))}
</SelectSetting>
<BooleanSetting
id="lightbox_scale_up"
headingID="dialogs.lightbox.scale_up.label"
subHeadingID="dialogs.lightbox.scale_up.description"
checked={iface.imageLightbox?.scaleUp ?? false}
onChange={(v) => saveLightboxSettings({ scaleUp: v })}
/>
<BooleanSetting
id="lightbox_reset_zoom_on_nav"
headingID="dialogs.lightbox.reset_zoom_on_nav"
checked={iface.imageLightbox?.resetZoomOnNav ?? false}
onChange={(v) => saveLightboxSettings({ resetZoomOnNav: v })}
/>
<SelectSetting
id="lightbox_scroll_mode"
headingID="dialogs.lightbox.scroll_mode.label"
subHeadingID="dialogs.lightbox.scroll_mode.description"
value={
iface.imageLightbox?.scrollMode ?? GQL.ImageLightboxScrollMode.Zoom
}
onChange={(v) =>
saveLightboxSettings({
scrollMode: v as GQL.ImageLightboxScrollMode,
})
}
>
{Array.from(imageLightboxScrollModeIntlMap.entries()).map((v) => (
<option key={v[0]} value={v[0]}>
{intl.formatMessage({
id: v[1],
})}
</option>
))}
</SelectSetting>
</SettingSection>
<SettingSection headingID="config.ui.editing.heading">

27
ui/v2.5/src/core/enums.ts Normal file
View file

@ -0,0 +1,27 @@
import {
ImageLightboxDisplayMode,
ImageLightboxScrollMode,
} from "../core/generated-graphql";
export const imageLightboxDisplayModeIntlMap = new Map<
ImageLightboxDisplayMode,
string
>([
[ImageLightboxDisplayMode.Original, "dialogs.lightbox.display_mode.original"],
[
ImageLightboxDisplayMode.FitXy,
"dialogs.lightbox.display_mode.fit_to_screen",
],
[
ImageLightboxDisplayMode.FitX,
"dialogs.lightbox.display_mode.fit_horizontally",
],
]);
export const imageLightboxScrollModeIntlMap = new Map<
ImageLightboxScrollMode,
string
>([
[ImageLightboxScrollMode.Zoom, "dialogs.lightbox.scroll_mode.zoom"],
[ImageLightboxScrollMode.PanY, "dialogs.lightbox.scroll_mode.pan_y"],
]);

View file

@ -15,7 +15,7 @@ import debounce from "lodash/debounce";
import { Icon, LoadingIndicator } from "src/components/Shared";
import { useInterval, usePageVisibility, useToast } from "src/hooks";
import { FormattedMessage, useIntl } from "react-intl";
import { DisplayMode, LightboxImage, ScrollMode } from "./LightboxImage";
import { LightboxImage } from "./LightboxImage";
import { ConfigurationContext } from "../Config";
import { Link } from "react-router-dom";
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
@ -27,6 +27,8 @@ import {
mutateImageResetO,
} from "src/core/StashService";
import * as GQL from "src/core/generated-graphql";
import { useInterfaceLocalForage } from "../LocalForage";
import { imageLightboxDisplayModeIntlMap } from "src/core/enums";
const CLASSNAME = "Lightbox";
const CLASSNAME_HEADER = `${CLASSNAME}-header`;
@ -98,12 +100,6 @@ export const LightboxComponent: React.FC<IProps> = ({
const oldImages = useRef<ILightboxImage[]>([]);
const [displayMode, setDisplayMode] = useState(DisplayMode.FIT_XY);
const oldDisplayMode = useRef(displayMode);
const [scaleUp, setScaleUp] = useState(false);
const [scrollMode, setScrollMode] = useState(ScrollMode.ZOOM);
const [resetZoomOnNav, setResetZoomOnNav] = useState(true);
const [zoom, setZoom] = useState(1);
const [resetPosition, setResetPosition] = useState(false);
@ -120,9 +116,53 @@ export const LightboxComponent: React.FC<IProps> = ({
const Toast = useToast();
const intl = useIntl();
const { configuration: config } = React.useContext(ConfigurationContext);
const [
interfaceLocalForage,
setInterfaceLocalForage,
] = useInterfaceLocalForage();
const userSelectedSlideshowDelayOrDefault =
config?.interface.slideshowDelay ?? DEFAULT_SLIDESHOW_DELAY;
const lightboxSettings = interfaceLocalForage.data?.imageLightbox;
function setLightboxSettings(v: Partial<GQL.ConfigImageLightboxInput>) {
setInterfaceLocalForage((prev) => {
return {
...prev,
imageLightbox: {
...prev.imageLightbox,
...v,
},
};
});
}
function setScaleUp(value: boolean) {
setLightboxSettings({ scaleUp: value });
}
function setResetZoomOnNav(v: boolean) {
setLightboxSettings({ resetZoomOnNav: v });
}
function setScrollMode(v: GQL.ImageLightboxScrollMode) {
setLightboxSettings({ scrollMode: v });
}
const slideshowDelay =
lightboxSettings?.slideshowDelay ??
config?.interface.imageLightbox.slideshowDelay ??
DEFAULT_SLIDESHOW_DELAY;
function setSlideshowDelay(v: number) {
setLightboxSettings({ slideshowDelay: v });
}
const displayMode =
lightboxSettings?.displayMode ?? GQL.ImageLightboxDisplayMode.FitXy;
const oldDisplayMode = useRef(displayMode);
function setDisplayMode(v: GQL.ImageLightboxDisplayMode) {
setLightboxSettings({ displayMode: v });
}
// slideshowInterval is used for controlling the logic
// displaySlideshowInterval is for display purposes only
@ -131,12 +171,11 @@ export const LightboxComponent: React.FC<IProps> = ({
const [slideshowInterval, setSlideshowInterval] = useState<number | null>(
null
);
const [
displayedSlideshowInterval,
setDisplayedSlideshowInterval,
] = useState<string>(
(userSelectedSlideshowDelayOrDefault / SECONDS_TO_MS).toString()
);
] = useState<string>(slideshowDelay.toString());
useEffect(() => {
if (images !== oldImages.current && isSwitchingPage) {
@ -164,7 +203,7 @@ export const LightboxComponent: React.FC<IProps> = ({
// reset zoom status
// setResetZoom((r) => !r);
// setZoomed(false);
if (resetZoomOnNav) {
if (lightboxSettings?.resetZoomOnNav) {
setZoom(1);
}
setResetPosition((r) => !r);
@ -192,20 +231,20 @@ export const LightboxComponent: React.FC<IProps> = ({
}
oldIndex.current = index;
}, [index, images.length, resetZoomOnNav]);
}, [index, images.length, lightboxSettings?.resetZoomOnNav]);
useEffect(() => {
if (displayMode !== oldDisplayMode.current) {
// reset zoom status
// setResetZoom((r) => !r);
// setZoomed(false);
if (resetZoomOnNav) {
if (lightboxSettings?.resetZoomOnNav) {
setZoom(1);
}
setResetPosition((r) => !r);
}
oldDisplayMode.current = displayMode;
}, [displayMode, resetZoomOnNav]);
}, [displayMode, lightboxSettings?.resetZoomOnNav]);
const selectIndex = (e: React.MouseEvent, i: number) => {
setIndex(i);
@ -224,20 +263,10 @@ export const LightboxComponent: React.FC<IProps> = ({
const toggleSlideshow = useCallback(() => {
if (slideshowInterval) {
setSlideshowInterval(null);
} else if (
displayedSlideshowInterval !== null &&
typeof displayedSlideshowInterval !== "undefined"
) {
const intervalNumber = Number.parseInt(displayedSlideshowInterval, 10);
setSlideshowInterval(intervalNumber * SECONDS_TO_MS);
} else {
setSlideshowInterval(userSelectedSlideshowDelayOrDefault);
setSlideshowInterval(slideshowDelay * SECONDS_TO_MS);
}
}, [
slideshowInterval,
userSelectedSlideshowDelayOrDefault,
displayedSlideshowInterval,
]);
}, [slideshowInterval, slideshowDelay]);
usePageVisibility(() => {
toggleSlideshow();
@ -352,10 +381,6 @@ export const LightboxComponent: React.FC<IProps> = ({
else document.exitFullscreen();
}, [isFullscreen]);
const handleSlideshowIntervalChange = (newSlideshowInterval: number) => {
setSlideshowInterval(newSlideshowInterval);
};
const navItems = images.map((image, i) => (
<img
src={image.paths.thumbnail ?? ""}
@ -372,19 +397,22 @@ export const LightboxComponent: React.FC<IProps> = ({
const onDelayChange = (e: React.ChangeEvent<HTMLInputElement>) => {
let numberValue = Number.parseInt(e.currentTarget.value, 10);
setDisplayedSlideshowInterval(e.currentTarget.value);
// Without this exception, the blocking of updates for invalid values is even weirder
if (e.currentTarget.value === "-" || e.currentTarget.value === "") {
setDisplayedSlideshowInterval(e.currentTarget.value);
return;
}
setDisplayedSlideshowInterval(e.currentTarget.value);
numberValue =
numberValue >= MIN_VALID_INTERVAL_SECONDS
? numberValue
: MIN_VALID_INTERVAL_SECONDS;
setSlideshowDelay(numberValue * SECONDS_TO_MS);
if (slideshowInterval !== null) {
numberValue =
numberValue >= MIN_VALID_INTERVAL_SECONDS
? numberValue
: MIN_VALID_INTERVAL_SECONDS;
handleSlideshowIntervalChange(numberValue * SECONDS_TO_MS);
setSlideshowInterval(numberValue * SECONDS_TO_MS);
}
};
@ -421,25 +449,19 @@ export const LightboxComponent: React.FC<IProps> = ({
<Col xs={8}>
<Form.Control
as="select"
onChange={(e) => setDisplayMode(e.target.value as DisplayMode)}
onChange={(e) =>
setDisplayMode(e.target.value as GQL.ImageLightboxDisplayMode)
}
value={displayMode}
className="btn-secondary mx-1 mb-1"
>
<option value={DisplayMode.ORIGINAL} key={DisplayMode.ORIGINAL}>
{intl.formatMessage({
id: "dialogs.lightbox.display_mode.original",
})}
</option>
<option value={DisplayMode.FIT_XY} key={DisplayMode.FIT_XY}>
{intl.formatMessage({
id: "dialogs.lightbox.display_mode.fit_to_screen",
})}
</option>
<option value={DisplayMode.FIT_X} key={DisplayMode.FIT_X}>
{intl.formatMessage({
id: "dialogs.lightbox.display_mode.fit_horizontally",
})}
</option>
{Array.from(imageLightboxDisplayModeIntlMap.entries()).map((v) => (
<option key={v[0]} value={v[0]}>
{intl.formatMessage({
id: v[1],
})}
</option>
))}
</Form.Control>
</Col>
</Form.Group>
@ -451,8 +473,8 @@ export const LightboxComponent: React.FC<IProps> = ({
label={intl.formatMessage({
id: "dialogs.lightbox.scale_up.label",
})}
checked={scaleUp}
disabled={displayMode === DisplayMode.ORIGINAL}
checked={lightboxSettings?.scaleUp ?? false}
disabled={displayMode === GQL.ImageLightboxDisplayMode.Original}
onChange={(v) => setScaleUp(v.currentTarget.checked)}
/>
</Col>
@ -471,7 +493,7 @@ export const LightboxComponent: React.FC<IProps> = ({
label={intl.formatMessage({
id: "dialogs.lightbox.reset_zoom_on_nav",
})}
checked={resetZoomOnNav}
checked={lightboxSettings?.resetZoomOnNav ?? false}
onChange={(v) => setResetZoomOnNav(v.currentTarget.checked)}
/>
</Col>
@ -487,16 +509,26 @@ export const LightboxComponent: React.FC<IProps> = ({
<Col xs={8}>
<Form.Control
as="select"
onChange={(e) => setScrollMode(e.target.value as ScrollMode)}
value={scrollMode}
onChange={(e) =>
setScrollMode(e.target.value as GQL.ImageLightboxScrollMode)
}
value={
lightboxSettings?.scrollMode ?? GQL.ImageLightboxScrollMode.Zoom
}
className="btn-secondary mx-1 mb-1"
>
<option value={ScrollMode.ZOOM} key={ScrollMode.ZOOM}>
<option
value={GQL.ImageLightboxScrollMode.Zoom}
key={GQL.ImageLightboxScrollMode.Zoom}
>
{intl.formatMessage({
id: "dialogs.lightbox.scroll_mode.zoom",
})}
</option>
<option value={ScrollMode.PAN_Y} key={ScrollMode.PAN_Y}>
<option
value={GQL.ImageLightboxScrollMode.PanY}
key={GQL.ImageLightboxScrollMode.PanY}
>
{intl.formatMessage({
id: "dialogs.lightbox.scroll_mode.pan_y",
})}
@ -686,8 +718,11 @@ export const LightboxComponent: React.FC<IProps> = ({
<LightboxImage
src={image.paths.image ?? ""}
displayMode={displayMode}
scaleUp={scaleUp}
scrollMode={scrollMode}
scaleUp={lightboxSettings?.scaleUp ?? false}
scrollMode={
lightboxSettings?.scrollMode ??
GQL.ImageLightboxScrollMode.Zoom
}
onLeft={handleLeft}
onRight={handleRight}
alignBottom={movingLeft}

View file

@ -1,4 +1,5 @@
import React, { useEffect, useRef, useState, useCallback } from "react";
import * as GQL from "src/core/generated-graphql";
const ZOOM_STEP = 1.1;
const SCROLL_PAN_STEP = 75;
@ -6,22 +7,11 @@ const CLASSNAME = "Lightbox";
const CLASSNAME_CAROUSEL = `${CLASSNAME}-carousel`;
const CLASSNAME_IMAGE = `${CLASSNAME_CAROUSEL}-image`;
export enum DisplayMode {
ORIGINAL = "ORIGINAL",
FIT_XY = "FIT_XY",
FIT_X = "FIT_X",
}
export enum ScrollMode {
ZOOM = "ZOOM",
PAN_Y = "PAN_Y",
}
interface IProps {
src: string;
displayMode: DisplayMode;
displayMode: GQL.ImageLightboxDisplayMode;
scaleUp: boolean;
scrollMode: ScrollMode;
scrollMode: GQL.ImageLightboxScrollMode;
resetPosition?: boolean;
zoom: number;
// set to true to align image with bottom instead of top
@ -105,7 +95,7 @@ export const LightboxImage: React.FC<IProps> = ({
let newZoom = 1;
let newPositionY = 0;
switch (displayMode) {
case DisplayMode.FIT_XY:
case GQL.ImageLightboxDisplayMode.FitXy:
xZoom = boxWidth / width;
yZoom = boxHeight / height;
@ -115,14 +105,14 @@ export const LightboxImage: React.FC<IProps> = ({
}
newZoom = Math.min(xZoom, yZoom);
break;
case DisplayMode.FIT_X:
case GQL.ImageLightboxDisplayMode.FitX:
newZoom = boxWidth / width;
if (!scaleUp) {
newZoom = Math.min(newZoom, 1);
}
break;
case DisplayMode.ORIGINAL:
case GQL.ImageLightboxDisplayMode.Original:
newZoom = 1;
break;
}
@ -131,7 +121,7 @@ export const LightboxImage: React.FC<IProps> = ({
const newPositionX = Math.min((boxWidth - width) / 2, 0);
// if fitting to screen, then centre, other
if (displayMode === DisplayMode.FIT_XY) {
if (displayMode === GQL.ImageLightboxDisplayMode.FitXy) {
newPositionY = Math.min((boxHeight - height) / 2, 0);
} else {
// otherwise, align top of image with container
@ -178,10 +168,10 @@ export const LightboxImage: React.FC<IProps> = ({
function getScrollMode(ev: React.WheelEvent<HTMLDivElement>) {
if (ev.shiftKey) {
switch (scrollMode) {
case ScrollMode.ZOOM:
return ScrollMode.PAN_Y;
case ScrollMode.PAN_Y:
return ScrollMode.ZOOM;
case GQL.ImageLightboxScrollMode.Zoom:
return GQL.ImageLightboxScrollMode.PanY;
case GQL.ImageLightboxScrollMode.PanY:
return GQL.ImageLightboxScrollMode.Zoom;
}
}
@ -190,7 +180,7 @@ export const LightboxImage: React.FC<IProps> = ({
function onContainerScroll(ev: React.WheelEvent<HTMLDivElement>) {
// don't zoom if mouse isn't over image
if (getScrollMode(ev) === ScrollMode.PAN_Y) {
if (getScrollMode(ev) === GQL.ImageLightboxScrollMode.PanY) {
onImageScroll(ev);
}
}
@ -244,10 +234,10 @@ export const LightboxImage: React.FC<IProps> = ({
const percent = ev.deltaY < 0 ? ZOOM_STEP : 1 / ZOOM_STEP;
switch (getScrollMode(ev)) {
case ScrollMode.ZOOM:
case GQL.ImageLightboxScrollMode.Zoom:
setZoom(zoom * percent);
break;
case ScrollMode.PAN_Y:
case GQL.ImageLightboxScrollMode.PanY:
onImageScrollPanY(ev);
break;
}

View file

@ -587,12 +587,18 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
if (level === PersistanceLevel.VIEW) {
setInterfaceState((prevState) => {
return {
[persistanceKey]: {
...prevState[persistanceKey],
filter: queryString.stringify({
...queryString.parse(prevState[persistanceKey]?.filter ?? ""),
disp: updatedFilter.displayMode,
}),
...prevState,
queryConfig: {
...prevState.queryConfig,
[persistanceKey]: {
...prevState.queryConfig[persistanceKey],
filter: queryString.stringify({
...queryString.parse(
prevState.queryConfig[persistanceKey]?.filter ?? ""
),
disp: updatedFilter.displayMode,
}),
},
},
};
});
@ -670,7 +676,7 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
}
// set the display type if persisted
const storedQuery = interfaceState.data?.[persistanceKey];
const storedQuery = interfaceState.data?.queryConfig?.[persistanceKey];
if (options.persistState === PersistanceLevel.VIEW && storedQuery) {
const storedFilter = queryString.parse(storedQuery.filter);

View file

@ -1,6 +1,7 @@
import localForage from "localforage";
import _ from "lodash";
import React, { Dispatch, SetStateAction, useEffect } from "react";
import { ConfigImageLightboxInput } from "src/core/generated-graphql";
interface IInterfaceQueryConfig {
filter: string;
@ -8,7 +9,12 @@ interface IInterfaceQueryConfig {
currentPage: number;
}
type IInterfaceConfig = Record<string, IInterfaceQueryConfig>;
type IQueryConfig = Record<string, IInterfaceQueryConfig>;
interface IInterfaceConfig {
queryConfig: IQueryConfig;
imageLightbox: ConfigImageLightboxInput;
}
export interface IChangelogConfig {
versions: Record<string, boolean>;

View file

@ -457,6 +457,9 @@
}
}
},
"image_lightbox": {
"heading": "Image Lightbox"
},
"interactive_options": "Interactive Options",
"language": {
"heading": "Language"