mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03: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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user