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 css
cssEnabled cssEnabled
language language
slideshowDelay imageLightbox {
slideshowDelay
displayMode
scaleUp
resetZoomOnNav
scrollMode
}
disableDropdownCreate { disableDropdownCreate {
performer performer
tag tag

View file

@ -196,6 +196,33 @@ input ConfigDisableDropdownCreateInput {
studio: Boolean 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 { input ConfigInterfaceInput {
"""Ordered list of items that should be shown in the menu""" """Ordered list of items that should be shown in the menu"""
menuItems: [String!] menuItems: [String!]
@ -229,9 +256,11 @@ input ConfigInterfaceInput {
"""Interface language""" """Interface language"""
language: String language: String
"""Slideshow Delay""" """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""" """Set to true to disable creating new objects via the dropdown menus"""
disableDropdownCreate: ConfigDisableDropdownCreateInput disableDropdownCreate: ConfigDisableDropdownCreateInput
@ -291,7 +320,9 @@ type ConfigInterfaceResult {
language: String language: String
"""Slideshow Delay""" """Slideshow Delay"""
slideshowDelay: Int slideshowDelay: Int @deprecated(reason: "Use imageLightbox.slideshowDelay")
imageLightbox: ConfigImageLightboxResult!
"""Fields are true if creating via dropdown menus are disabled""" """Fields are true if creating via dropdown menus are disabled"""
disableDropdownCreate: ConfigDisableDropdownCreate! 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 { if input.MenuItems != nil {
c.Set(config.MenuItems, input.MenuItems) 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) c.Set(config.Language, *input.Language)
} }
// deprecated field
if input.SlideshowDelay != nil { 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 { if input.CSS != nil {

View file

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

View file

@ -142,8 +142,15 @@ const (
WallPlayback = "wall_playback" WallPlayback = "wall_playback"
defaultWallPlayback = "video" defaultWallPlayback = "video"
SlideshowDelay = "slideshow_delay" // Image lightbox options
defaultSlideshowDelay = 5000 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" DisableDropdownCreatePerformer = "disable_dropdown_create.performer"
DisableDropdownCreateStudio = "disable_dropdown_create.studio" DisableDropdownCreateStudio = "disable_dropdown_create.studio"
@ -364,6 +371,18 @@ func (i *Instance) viper(key string) *viper.Viper {
return v 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 { func (i *Instance) HasOverride(key string) bool {
i.RLock() i.RLock()
defer i.RUnlock() defer i.RUnlock()
@ -886,14 +905,49 @@ func (i *Instance) GetShowStudioAsText() bool {
return i.getBool(ShowStudioAsText) 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() i.RLock()
defer i.RUnlock() defer i.RUnlock()
ret := defaultSlideshowDelay delay := i.getSlideshowDelay()
v := i.viper(SlideshowDelay)
if v.IsSet(SlideshowDelay) { ret := models.ConfigImageLightboxResult{
ret = v.GetInt(SlideshowDelay) 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 return ret

View file

@ -81,7 +81,8 @@ func TestConcurrentConfigAccess(t *testing.T) {
i.Set(MaximumLoopDuration, i.GetMaximumLoopDuration()) i.Set(MaximumLoopDuration, i.GetMaximumLoopDuration())
i.Set(AutostartVideo, i.GetAutostartVideo()) i.Set(AutostartVideo, i.GetAutostartVideo())
i.Set(ShowStudioAsText, i.GetShowStudioAsText()) 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.GetCSSPath()
i.GetCSS() i.GetCSS()
i.Set(CSSEnabled, i.GetCSSEnabled()) i.Set(CSSEnabled, i.GetCSSEnabled())

View file

@ -1,4 +1,5 @@
### 🎨 Improvements ### 🎨 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)) * 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)) * 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)) * Improved autotag performance. ([#2368](https://github.com/stashapp/stash/pull/2368))

View file

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

View file

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

View file

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

View file

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

View file

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