mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
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:
parent
4c4cdae1ed
commit
2afb467bb1
14 changed files with 382 additions and 114 deletions
|
|
@ -62,7 +62,13 @@ fragment ConfigInterfaceData on ConfigInterfaceResult {
|
|||
css
|
||||
cssEnabled
|
||||
language
|
||||
slideshowDelay
|
||||
imageLightbox {
|
||||
slideshowDelay
|
||||
displayMode
|
||||
scaleUp
|
||||
resetZoomOnNav
|
||||
scrollMode
|
||||
}
|
||||
disableDropdownCreate {
|
||||
performer
|
||||
tag
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
27
ui/v2.5/src/core/enums.ts
Normal 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"],
|
||||
]);
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -457,6 +457,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"image_lightbox": {
|
||||
"heading": "Image Lightbox"
|
||||
},
|
||||
"interactive_options": "Interactive Options",
|
||||
"language": {
|
||||
"heading": "Language"
|
||||
|
|
|
|||
Loading…
Reference in a new issue