mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Add title, rating, o-counter to image lightbox (#2274)
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
### ✨ New Features
|
### ✨ New Features
|
||||||
|
* Added title, rating and o-counter in image lightbox. ([#2274](https://github.com/stashapp/stash/pull/2274))
|
||||||
* Added option to hide scene scrubber by default. ([#2325](https://github.com/stashapp/stash/pull/2325))
|
* Added option to hide scene scrubber by default. ([#2325](https://github.com/stashapp/stash/pull/2325))
|
||||||
* Added support for bulk-editing movies. ([#2283](https://github.com/stashapp/stash/pull/2283))
|
* Added support for bulk-editing movies. ([#2283](https://github.com/stashapp/stash/pull/2283))
|
||||||
* Added support for filtering scenes, images and galleries featuring favourite performers and performer age at time of production. ([#2257](https://github.com/stashapp/stash/pull/2257))
|
* Added support for filtering scenes, images and galleries featuring favourite performers and performer age at time of production. ([#2257](https://github.com/stashapp/stash/pull/2257))
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ export const Image: React.FC = () => {
|
|||||||
|
|
||||||
const { data, error, loading } = useFindImage(id);
|
const { data, error, loading } = useFindImage(id);
|
||||||
const image = data?.findImage;
|
const image = data?.findImage;
|
||||||
const [oLoading, setOLoading] = useState(false);
|
|
||||||
const [incrementO] = useImageIncrementO(image?.id ?? "0");
|
const [incrementO] = useImageIncrementO(image?.id ?? "0");
|
||||||
const [decrementO] = useImageDecrementO(image?.id ?? "0");
|
const [decrementO] = useImageDecrementO(image?.id ?? "0");
|
||||||
const [resetO] = useImageResetO(image?.id ?? "0");
|
const [resetO] = useImageResetO(image?.id ?? "0");
|
||||||
@@ -87,34 +86,25 @@ export const Image: React.FC = () => {
|
|||||||
|
|
||||||
const onIncrementClick = async () => {
|
const onIncrementClick = async () => {
|
||||||
try {
|
try {
|
||||||
setOLoading(true);
|
|
||||||
await incrementO();
|
await incrementO();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
} finally {
|
|
||||||
setOLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDecrementClick = async () => {
|
const onDecrementClick = async () => {
|
||||||
try {
|
try {
|
||||||
setOLoading(true);
|
|
||||||
await decrementO();
|
await decrementO();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
} finally {
|
|
||||||
setOLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onResetClick = async () => {
|
const onResetClick = async () => {
|
||||||
try {
|
try {
|
||||||
setOLoading(true);
|
|
||||||
await resetO();
|
await resetO();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
} finally {
|
|
||||||
setOLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -196,7 +186,6 @@ export const Image: React.FC = () => {
|
|||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
<Nav.Item className="ml-auto">
|
<Nav.Item className="ml-auto">
|
||||||
<OCounterButton
|
<OCounterButton
|
||||||
loading={oLoading}
|
|
||||||
value={image.o_counter || 0}
|
value={image.o_counter || 0}
|
||||||
onIncrement={onIncrementClick}
|
onIncrement={onIncrementClick}
|
||||||
onDecrement={onDecrementClick}
|
onDecrement={onDecrementClick}
|
||||||
|
|||||||
@@ -1,34 +1,45 @@
|
|||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import {
|
import { Button, ButtonGroup, Dropdown, DropdownButton } from "react-bootstrap";
|
||||||
Button,
|
|
||||||
ButtonGroup,
|
|
||||||
Dropdown,
|
|
||||||
DropdownButton,
|
|
||||||
Spinner,
|
|
||||||
} from "react-bootstrap";
|
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { Icon, SweatDrops } from "src/components/Shared";
|
import { Icon, LoadingIndicator, SweatDrops } from "src/components/Shared";
|
||||||
|
|
||||||
export interface IOCounterButtonProps {
|
export interface IOCounterButtonProps {
|
||||||
loading: boolean;
|
|
||||||
value: number;
|
value: number;
|
||||||
onIncrement: () => void;
|
onIncrement: () => Promise<void>;
|
||||||
onDecrement: () => void;
|
onDecrement: () => Promise<void>;
|
||||||
onReset: () => void;
|
onReset: () => Promise<void>;
|
||||||
onMenuOpened?: () => void;
|
|
||||||
onMenuClosed?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OCounterButton: React.FC<IOCounterButtonProps> = (
|
export const OCounterButton: React.FC<IOCounterButtonProps> = (
|
||||||
props: IOCounterButtonProps
|
props: IOCounterButtonProps
|
||||||
) => {
|
) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
if (props.loading) return <Spinner animation="border" role="status" />;
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
async function increment() {
|
||||||
|
setLoading(true);
|
||||||
|
await props.onIncrement();
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function decrement() {
|
||||||
|
setLoading(true);
|
||||||
|
await props.onDecrement();
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reset() {
|
||||||
|
setLoading(true);
|
||||||
|
await props.onReset();
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) return <LoadingIndicator message="" inline small />;
|
||||||
|
|
||||||
const renderButton = () => (
|
const renderButton = () => (
|
||||||
<Button
|
<Button
|
||||||
className="minimal pr-1"
|
className="minimal pr-1"
|
||||||
onClick={props.onIncrement}
|
onClick={increment}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
title={intl.formatMessage({ id: "o_counter" })}
|
title={intl.formatMessage({ id: "o_counter" })}
|
||||||
>
|
>
|
||||||
@@ -46,11 +57,11 @@ export const OCounterButton: React.FC<IOCounterButtonProps> = (
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="pl-0 show-carat"
|
className="pl-0 show-carat"
|
||||||
>
|
>
|
||||||
<Dropdown.Item onClick={props.onDecrement}>
|
<Dropdown.Item onClick={decrement}>
|
||||||
<Icon icon="minus" />
|
<Icon icon="minus" />
|
||||||
<span>Decrement</span>
|
<span>Decrement</span>
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
<Dropdown.Item onClick={props.onReset}>
|
<Dropdown.Item onClick={reset}>
|
||||||
<Icon icon="ban" />
|
<Icon icon="ban" />
|
||||||
<span>Reset</span>
|
<span>Reset</span>
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ const ScenePage: React.FC<IProps> = ({ scene, refetch }) => {
|
|||||||
loading: streamableLoading,
|
loading: streamableLoading,
|
||||||
} = useSceneStreams(scene.id);
|
} = useSceneStreams(scene.id);
|
||||||
|
|
||||||
const [oLoading, setOLoading] = useState(false);
|
|
||||||
const [incrementO] = useSceneIncrementO(scene.id);
|
const [incrementO] = useSceneIncrementO(scene.id);
|
||||||
const [decrementO] = useSceneDecrementO(scene.id);
|
const [decrementO] = useSceneDecrementO(scene.id);
|
||||||
const [resetO] = useSceneResetO(scene.id);
|
const [resetO] = useSceneResetO(scene.id);
|
||||||
@@ -172,34 +171,25 @@ const ScenePage: React.FC<IProps> = ({ scene, refetch }) => {
|
|||||||
|
|
||||||
const onIncrementClick = async () => {
|
const onIncrementClick = async () => {
|
||||||
try {
|
try {
|
||||||
setOLoading(true);
|
|
||||||
await incrementO();
|
await incrementO();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
} finally {
|
|
||||||
setOLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDecrementClick = async () => {
|
const onDecrementClick = async () => {
|
||||||
try {
|
try {
|
||||||
setOLoading(true);
|
|
||||||
await decrementO();
|
await decrementO();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
} finally {
|
|
||||||
setOLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onResetClick = async () => {
|
const onResetClick = async () => {
|
||||||
try {
|
try {
|
||||||
setOLoading(true);
|
|
||||||
await resetO();
|
await resetO();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
} finally {
|
|
||||||
setOLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -485,7 +475,6 @@ const ScenePage: React.FC<IProps> = ({ scene, refetch }) => {
|
|||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
<Nav.Item className="ml-auto">
|
<Nav.Item className="ml-auto">
|
||||||
<OCounterButton
|
<OCounterButton
|
||||||
loading={oLoading}
|
|
||||||
value={scene.o_counter || 0}
|
value={scene.o_counter || 0}
|
||||||
onIncrement={onIncrementClick}
|
onIncrement={onIncrementClick}
|
||||||
onDecrement={onDecrementClick}
|
onDecrement={onDecrementClick}
|
||||||
|
|||||||
@@ -541,21 +541,64 @@ const updateImageO = (
|
|||||||
export const useImageIncrementO = (id: string) =>
|
export const useImageIncrementO = (id: string) =>
|
||||||
GQL.useImageIncrementOMutation({
|
GQL.useImageIncrementOMutation({
|
||||||
variables: { id },
|
variables: { id },
|
||||||
update: (cache, data) =>
|
update: (cache, data) => {
|
||||||
updateImageO(id, cache, data.data?.imageIncrementO),
|
updateImageO(id, cache, data.data?.imageIncrementO);
|
||||||
|
// impacts FindImages as well as FindImage
|
||||||
|
deleteCache([GQL.FindImagesDocument])(cache);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mutateImageIncrementO = (id: string) =>
|
||||||
|
client.mutate<GQL.ImageIncrementOMutation>({
|
||||||
|
mutation: GQL.ImageIncrementODocument,
|
||||||
|
variables: { id },
|
||||||
|
update: (cache, data) => {
|
||||||
|
updateImageO(id, cache, data.data?.imageIncrementO);
|
||||||
|
// impacts FindImages as well as FindImage
|
||||||
|
deleteCache([GQL.FindImagesDocument])(cache);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useImageDecrementO = (id: string) =>
|
export const useImageDecrementO = (id: string) =>
|
||||||
GQL.useImageDecrementOMutation({
|
GQL.useImageDecrementOMutation({
|
||||||
variables: { id },
|
variables: { id },
|
||||||
update: (cache, data) =>
|
update: (cache, data) => {
|
||||||
updateImageO(id, cache, data.data?.imageDecrementO),
|
updateImageO(id, cache, data.data?.imageDecrementO);
|
||||||
|
// impacts FindImages as well as FindImage
|
||||||
|
deleteCache([GQL.FindImagesDocument])(cache);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mutateImageDecrementO = (id: string) =>
|
||||||
|
client.mutate<GQL.ImageDecrementOMutation>({
|
||||||
|
mutation: GQL.ImageDecrementODocument,
|
||||||
|
variables: { id },
|
||||||
|
update: (cache, data) => {
|
||||||
|
updateImageO(id, cache, data.data?.imageDecrementO);
|
||||||
|
// impacts FindImages as well as FindImage
|
||||||
|
deleteCache([GQL.FindImagesDocument])(cache);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useImageResetO = (id: string) =>
|
export const useImageResetO = (id: string) =>
|
||||||
GQL.useImageResetOMutation({
|
GQL.useImageResetOMutation({
|
||||||
variables: { id },
|
variables: { id },
|
||||||
update: (cache, data) => updateImageO(id, cache, data.data?.imageResetO),
|
update: (cache, data) => {
|
||||||
|
updateImageO(id, cache, data.data?.imageResetO);
|
||||||
|
// impacts FindImages as well as FindImage
|
||||||
|
deleteCache([GQL.FindImagesDocument])(cache);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mutateImageResetO = (id: string) =>
|
||||||
|
client.mutate<GQL.ImageResetOMutation>({
|
||||||
|
mutation: GQL.ImageResetODocument,
|
||||||
|
variables: { id },
|
||||||
|
update: (cache, data) => {
|
||||||
|
updateImageO(id, cache, data.data?.imageResetO);
|
||||||
|
// impacts FindImages as well as FindImage
|
||||||
|
deleteCache([GQL.FindImagesDocument])(cache);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const galleryMutationImpactedQueries = [
|
const galleryMutationImpactedQueries = [
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Col,
|
Col,
|
||||||
@@ -14,10 +13,20 @@ import Mousetrap from "mousetrap";
|
|||||||
import debounce from "lodash/debounce";
|
import debounce from "lodash/debounce";
|
||||||
|
|
||||||
import { Icon, LoadingIndicator } from "src/components/Shared";
|
import { Icon, LoadingIndicator } from "src/components/Shared";
|
||||||
import { useInterval, usePageVisibility } 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 { DisplayMode, LightboxImage, ScrollMode } from "./LightboxImage";
|
||||||
import { ConfigurationContext } from "../Config";
|
import { ConfigurationContext } from "../Config";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
||||||
|
import { OCounterButton } from "src/components/Scenes/SceneDetails/OCounterButton";
|
||||||
|
import {
|
||||||
|
useImageUpdate,
|
||||||
|
mutateImageIncrementO,
|
||||||
|
mutateImageDecrementO,
|
||||||
|
mutateImageResetO,
|
||||||
|
} from "src/core/StashService";
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
|
||||||
const CLASSNAME = "Lightbox";
|
const CLASSNAME = "Lightbox";
|
||||||
const CLASSNAME_HEADER = `${CLASSNAME}-header`;
|
const CLASSNAME_HEADER = `${CLASSNAME}-header`;
|
||||||
@@ -27,6 +36,8 @@ const CLASSNAME_OPTIONS = `${CLASSNAME_HEADER}-options`;
|
|||||||
const CLASSNAME_OPTIONS_ICON = `${CLASSNAME_OPTIONS}-icon`;
|
const CLASSNAME_OPTIONS_ICON = `${CLASSNAME_OPTIONS}-icon`;
|
||||||
const CLASSNAME_OPTIONS_INLINE = `${CLASSNAME_OPTIONS}-inline`;
|
const CLASSNAME_OPTIONS_INLINE = `${CLASSNAME_OPTIONS}-inline`;
|
||||||
const CLASSNAME_RIGHT = `${CLASSNAME_HEADER}-right`;
|
const CLASSNAME_RIGHT = `${CLASSNAME_HEADER}-right`;
|
||||||
|
const CLASSNAME_FOOTER = `${CLASSNAME}-footer`;
|
||||||
|
const CLASSNAME_FOOTER_LEFT = `${CLASSNAME_FOOTER}-left`;
|
||||||
const CLASSNAME_DISPLAY = `${CLASSNAME}-display`;
|
const CLASSNAME_DISPLAY = `${CLASSNAME}-display`;
|
||||||
const CLASSNAME_CAROUSEL = `${CLASSNAME}-carousel`;
|
const CLASSNAME_CAROUSEL = `${CLASSNAME}-carousel`;
|
||||||
const CLASSNAME_INSTANT = `${CLASSNAME_CAROUSEL}-instant`;
|
const CLASSNAME_INSTANT = `${CLASSNAME_CAROUSEL}-instant`;
|
||||||
@@ -40,9 +51,20 @@ const DEFAULT_SLIDESHOW_DELAY = 5000;
|
|||||||
const SECONDS_TO_MS = 1000;
|
const SECONDS_TO_MS = 1000;
|
||||||
const MIN_VALID_INTERVAL_SECONDS = 1;
|
const MIN_VALID_INTERVAL_SECONDS = 1;
|
||||||
|
|
||||||
type Image = Pick<GQL.Image, "paths">;
|
interface IImagePaths {
|
||||||
|
image?: GQL.Maybe<string>;
|
||||||
|
thumbnail?: GQL.Maybe<string>;
|
||||||
|
}
|
||||||
|
export interface ILightboxImage {
|
||||||
|
id?: string;
|
||||||
|
title?: GQL.Maybe<string>;
|
||||||
|
rating?: GQL.Maybe<number>;
|
||||||
|
o_counter?: GQL.Maybe<number>;
|
||||||
|
paths: IImagePaths;
|
||||||
|
}
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
images: Image[];
|
images: ILightboxImage[];
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
initialIndex?: number;
|
initialIndex?: number;
|
||||||
@@ -64,6 +86,8 @@ export const LightboxComponent: React.FC<IProps> = ({
|
|||||||
pageCallback,
|
pageCallback,
|
||||||
hide,
|
hide,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [updateImage] = useImageUpdate();
|
||||||
|
|
||||||
const [index, setIndex] = useState<number | null>(null);
|
const [index, setIndex] = useState<number | null>(null);
|
||||||
const oldIndex = useRef<number | null>(null);
|
const oldIndex = useRef<number | null>(null);
|
||||||
const [instantTransition, setInstantTransition] = useState(false);
|
const [instantTransition, setInstantTransition] = useState(false);
|
||||||
@@ -71,7 +95,7 @@ export const LightboxComponent: React.FC<IProps> = ({
|
|||||||
const [isFullscreen, setFullscreen] = useState(false);
|
const [isFullscreen, setFullscreen] = useState(false);
|
||||||
const [showOptions, setShowOptions] = useState(false);
|
const [showOptions, setShowOptions] = useState(false);
|
||||||
|
|
||||||
const oldImages = useRef<Image[]>([]);
|
const oldImages = useRef<ILightboxImage[]>([]);
|
||||||
|
|
||||||
const [displayMode, setDisplayMode] = useState(DisplayMode.FIT_XY);
|
const [displayMode, setDisplayMode] = useState(DisplayMode.FIT_XY);
|
||||||
const oldDisplayMode = useRef(displayMode);
|
const oldDisplayMode = useRef(displayMode);
|
||||||
@@ -92,6 +116,7 @@ export const LightboxComponent: React.FC<IProps> = ({
|
|||||||
|
|
||||||
const allowNavigation = images.length > 1 || pageCallback;
|
const allowNavigation = images.length > 1 || pageCallback;
|
||||||
|
|
||||||
|
const Toast = useToast();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { configuration: config } = React.useContext(ConfigurationContext);
|
const { configuration: config } = React.useContext(ConfigurationContext);
|
||||||
|
|
||||||
@@ -496,170 +521,236 @@ export const LightboxComponent: React.FC<IProps> = ({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const element = isVisible ? (
|
if (!isVisible) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (images.length === 0 || isLoading || isSwitchingPage) {
|
||||||
|
return <LoadingIndicator />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentImage = images[currentIndex];
|
||||||
|
|
||||||
|
function setRating(v: number | null) {
|
||||||
|
if (currentImage?.id) {
|
||||||
|
updateImage({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
id: currentImage.id,
|
||||||
|
rating: v,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onIncrementClick() {
|
||||||
|
if (currentImage.id === undefined) return;
|
||||||
|
try {
|
||||||
|
await mutateImageIncrementO(currentImage.id);
|
||||||
|
} catch (e) {
|
||||||
|
Toast.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onDecrementClick() {
|
||||||
|
if (currentImage.id === undefined) return;
|
||||||
|
try {
|
||||||
|
await mutateImageDecrementO(currentImage.id);
|
||||||
|
} catch (e) {
|
||||||
|
Toast.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onResetClick() {
|
||||||
|
if (currentImage.id === undefined) return;
|
||||||
|
try {
|
||||||
|
await mutateImageResetO(currentImage.id);
|
||||||
|
} catch (e) {
|
||||||
|
Toast.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
className={CLASSNAME}
|
className={CLASSNAME}
|
||||||
role="presentation"
|
role="presentation"
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
>
|
>
|
||||||
{images.length > 0 && !isLoading && !isSwitchingPage ? (
|
<div className={CLASSNAME_HEADER}>
|
||||||
<>
|
<div className={CLASSNAME_LEFT_SPACER} />
|
||||||
<div className={CLASSNAME_HEADER}>
|
<div className={CLASSNAME_INDICATOR}>
|
||||||
<div className={CLASSNAME_LEFT_SPACER} />
|
<span>{pageHeader}</span>
|
||||||
<div className={CLASSNAME_INDICATOR}>
|
{images.length > 1 ? (
|
||||||
<span>{pageHeader}</span>
|
<b ref={indicatorRef}>{`${currentIndex + 1} / ${images.length}`}</b>
|
||||||
<b ref={indicatorRef}>
|
) : undefined}
|
||||||
{`${currentIndex + 1} / ${images.length}`}
|
</div>
|
||||||
</b>
|
<div className={CLASSNAME_RIGHT}>
|
||||||
</div>
|
<div className={CLASSNAME_OPTIONS}>
|
||||||
<div className={CLASSNAME_RIGHT}>
|
<div className={CLASSNAME_OPTIONS_ICON}>
|
||||||
<div className={CLASSNAME_OPTIONS}>
|
|
||||||
<div className={CLASSNAME_OPTIONS_ICON}>
|
|
||||||
<Button
|
|
||||||
ref={overlayTarget}
|
|
||||||
variant="link"
|
|
||||||
title="Options"
|
|
||||||
onClick={() => setShowOptions(!showOptions)}
|
|
||||||
>
|
|
||||||
<Icon icon="cog" />
|
|
||||||
</Button>
|
|
||||||
<Overlay
|
|
||||||
target={overlayTarget.current}
|
|
||||||
show={showOptions}
|
|
||||||
placement="bottom"
|
|
||||||
container={containerRef}
|
|
||||||
rootClose
|
|
||||||
onHide={() => setShowOptions(false)}
|
|
||||||
>
|
|
||||||
{({ placement, arrowProps, show: _show, ...props }) => (
|
|
||||||
<div
|
|
||||||
className="popover"
|
|
||||||
{...props}
|
|
||||||
style={{ ...props.style }}
|
|
||||||
>
|
|
||||||
{optionsPopover}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Overlay>
|
|
||||||
</div>
|
|
||||||
<InputGroup className={CLASSNAME_OPTIONS_INLINE}>
|
|
||||||
<OptionsForm />
|
|
||||||
</InputGroup>
|
|
||||||
</div>
|
|
||||||
{slideshowEnabled && (
|
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
onClick={toggleSlideshow}
|
|
||||||
title="Toggle Slideshow"
|
|
||||||
>
|
|
||||||
<Icon icon={slideshowInterval !== null ? "pause" : "play"} />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{zoom !== 1 && (
|
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
onClick={() => {
|
|
||||||
setResetPosition(!resetPosition);
|
|
||||||
setZoom(1);
|
|
||||||
}}
|
|
||||||
title="Reset zoom"
|
|
||||||
>
|
|
||||||
<Icon icon="search-minus" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{document.fullscreenEnabled && (
|
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
onClick={toggleFullscreen}
|
|
||||||
title="Toggle Fullscreen"
|
|
||||||
>
|
|
||||||
<Icon icon="expand" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
<Button
|
||||||
|
ref={overlayTarget}
|
||||||
variant="link"
|
variant="link"
|
||||||
onClick={() => close()}
|
title="Options"
|
||||||
title="Close Lightbox"
|
onClick={() => setShowOptions(!showOptions)}
|
||||||
>
|
>
|
||||||
<Icon icon="times" />
|
<Icon icon="cog" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Overlay
|
||||||
|
target={overlayTarget.current}
|
||||||
|
show={showOptions}
|
||||||
|
placement="bottom"
|
||||||
|
container={containerRef}
|
||||||
|
rootClose
|
||||||
|
onHide={() => setShowOptions(false)}
|
||||||
|
>
|
||||||
|
{({ placement, arrowProps, show: _show, ...props }) => (
|
||||||
|
<div
|
||||||
|
className="popover"
|
||||||
|
{...props}
|
||||||
|
style={{ ...props.style }}
|
||||||
|
>
|
||||||
|
{optionsPopover}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Overlay>
|
||||||
</div>
|
</div>
|
||||||
|
<InputGroup className={CLASSNAME_OPTIONS_INLINE}>
|
||||||
|
<OptionsForm />
|
||||||
|
</InputGroup>
|
||||||
</div>
|
</div>
|
||||||
<div className={CLASSNAME_DISPLAY}>
|
{slideshowEnabled && (
|
||||||
{allowNavigation && (
|
<Button
|
||||||
<Button
|
variant="link"
|
||||||
variant="link"
|
onClick={toggleSlideshow}
|
||||||
onClick={handleLeft}
|
title="Toggle Slideshow"
|
||||||
className={`${CLASSNAME_NAVBUTTON} d-none d-lg-block`}
|
|
||||||
>
|
|
||||||
<Icon icon="chevron-left" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={cx(CLASSNAME_CAROUSEL, {
|
|
||||||
[CLASSNAME_INSTANT]: instantTransition,
|
|
||||||
})}
|
|
||||||
style={{ left: `${currentIndex * -100}vw` }}
|
|
||||||
ref={carouselRef}
|
|
||||||
>
|
>
|
||||||
{images.map((image, i) => (
|
<Icon icon={slideshowInterval !== null ? "pause" : "play"} />
|
||||||
<div className={`${CLASSNAME_IMAGE}`} key={image.paths.image}>
|
</Button>
|
||||||
{i >= currentIndex - 1 && i <= currentIndex + 1 ? (
|
|
||||||
<LightboxImage
|
|
||||||
src={image.paths.image ?? ""}
|
|
||||||
displayMode={displayMode}
|
|
||||||
scaleUp={scaleUp}
|
|
||||||
scrollMode={scrollMode}
|
|
||||||
onLeft={handleLeft}
|
|
||||||
onRight={handleRight}
|
|
||||||
zoom={i === currentIndex ? zoom : 1}
|
|
||||||
setZoom={(v) => setZoom(v)}
|
|
||||||
resetPosition={resetPosition}
|
|
||||||
/>
|
|
||||||
) : undefined}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{allowNavigation && (
|
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
onClick={handleRight}
|
|
||||||
className={`${CLASSNAME_NAVBUTTON} d-none d-lg-block`}
|
|
||||||
>
|
|
||||||
<Icon icon="chevron-right" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{showNavigation && !isFullscreen && images.length > 1 && (
|
|
||||||
<div className={CLASSNAME_NAV} ref={navRef}>
|
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
onClick={() => setIndex(images.length - 1)}
|
|
||||||
className={CLASSNAME_NAVBUTTON}
|
|
||||||
>
|
|
||||||
<Icon icon="arrow-left" className="mr-4" />
|
|
||||||
</Button>
|
|
||||||
{navItems}
|
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
onClick={() => setIndex(0)}
|
|
||||||
className={CLASSNAME_NAVBUTTON}
|
|
||||||
>
|
|
||||||
<Icon icon="arrow-right" className="ml-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</>
|
{zoom !== 1 && (
|
||||||
) : (
|
<Button
|
||||||
<LoadingIndicator />
|
variant="link"
|
||||||
)}
|
onClick={() => {
|
||||||
</div>
|
setResetPosition(!resetPosition);
|
||||||
) : (
|
setZoom(1);
|
||||||
<></>
|
}}
|
||||||
);
|
title="Reset zoom"
|
||||||
|
>
|
||||||
|
<Icon icon="search-minus" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{document.fullscreenEnabled && (
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
onClick={toggleFullscreen}
|
||||||
|
title="Toggle Fullscreen"
|
||||||
|
>
|
||||||
|
<Icon icon="expand" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button variant="link" onClick={() => close()} title="Close Lightbox">
|
||||||
|
<Icon icon="times" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={CLASSNAME_DISPLAY}>
|
||||||
|
{allowNavigation && (
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
onClick={handleLeft}
|
||||||
|
className={`${CLASSNAME_NAVBUTTON} d-none d-lg-block`}
|
||||||
|
>
|
||||||
|
<Icon icon="chevron-left" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
return element;
|
<div
|
||||||
|
className={cx(CLASSNAME_CAROUSEL, {
|
||||||
|
[CLASSNAME_INSTANT]: instantTransition,
|
||||||
|
})}
|
||||||
|
style={{ left: `${currentIndex * -100}vw` }}
|
||||||
|
ref={carouselRef}
|
||||||
|
>
|
||||||
|
{images.map((image, i) => (
|
||||||
|
<div className={`${CLASSNAME_IMAGE}`} key={image.paths.image}>
|
||||||
|
{i >= currentIndex - 1 && i <= currentIndex + 1 ? (
|
||||||
|
<LightboxImage
|
||||||
|
src={image.paths.image ?? ""}
|
||||||
|
displayMode={displayMode}
|
||||||
|
scaleUp={scaleUp}
|
||||||
|
scrollMode={scrollMode}
|
||||||
|
onLeft={handleLeft}
|
||||||
|
onRight={handleRight}
|
||||||
|
zoom={i === currentIndex ? zoom : 1}
|
||||||
|
setZoom={(v) => setZoom(v)}
|
||||||
|
resetPosition={resetPosition}
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{allowNavigation && (
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
onClick={handleRight}
|
||||||
|
className={`${CLASSNAME_NAVBUTTON} d-none d-lg-block`}
|
||||||
|
>
|
||||||
|
<Icon icon="chevron-right" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{showNavigation && !isFullscreen && images.length > 1 && (
|
||||||
|
<div className={CLASSNAME_NAV} ref={navRef}>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
onClick={() => setIndex(images.length - 1)}
|
||||||
|
className={CLASSNAME_NAVBUTTON}
|
||||||
|
>
|
||||||
|
<Icon icon="arrow-left" className="mr-4" />
|
||||||
|
</Button>
|
||||||
|
{navItems}
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
onClick={() => setIndex(0)}
|
||||||
|
className={CLASSNAME_NAVBUTTON}
|
||||||
|
>
|
||||||
|
<Icon icon="arrow-right" className="ml-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={CLASSNAME_FOOTER}>
|
||||||
|
<div className={CLASSNAME_FOOTER_LEFT}>
|
||||||
|
{currentImage.id !== undefined && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<OCounterButton
|
||||||
|
onDecrement={onDecrementClick}
|
||||||
|
onIncrement={onIncrementClick}
|
||||||
|
onReset={onResetClick}
|
||||||
|
value={currentImage.o_counter ?? 0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<RatingStars
|
||||||
|
value={currentImage.rating ?? undefined}
|
||||||
|
onSetRating={(v) => {
|
||||||
|
setRating(v ?? null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{currentImage.title && (
|
||||||
|
<Link to={`/images/${currentImage.id}`} onClick={() => hide()}>
|
||||||
|
{currentImage.title ?? ""}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import React, { useCallback, useState } from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import { ILightboxImage, LightboxComponent } from "./Lightbox";
|
||||||
import { LightboxComponent } from "./Lightbox";
|
|
||||||
|
|
||||||
type Image = Pick<GQL.Image, "paths">;
|
|
||||||
|
|
||||||
export interface IState {
|
export interface IState {
|
||||||
images: Image[];
|
images: ILightboxImage[];
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
showNavigation: boolean;
|
showNavigation: boolean;
|
||||||
|
|||||||
@@ -9,23 +9,23 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
z-index: 1040;
|
z-index: 1040;
|
||||||
|
|
||||||
.fa-icon {
|
|
||||||
path {
|
|
||||||
fill: white;
|
|
||||||
}
|
|
||||||
opacity: 0.4;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-header {
|
&-header {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
height: 4rem;
|
height: 4rem;
|
||||||
|
|
||||||
|
.fa-icon {
|
||||||
|
path {
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
opacity: 0.4;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&-left-spacer {
|
&-left-spacer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -72,11 +72,42 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-footer {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 4.5rem;
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-stars {
|
||||||
|
padding-left: 0.38rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: start;
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $text-color;
|
||||||
|
font-weight: bold;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&-display {
|
&-display {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 2rem;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +156,16 @@
|
|||||||
|
|
||||||
.fa-icon {
|
.fa-icon {
|
||||||
height: 4rem;
|
height: 4rem;
|
||||||
|
opacity: 0.4;
|
||||||
width: 4rem;
|
width: 4rem;
|
||||||
|
|
||||||
|
path {
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
|
|||||||
Reference in New Issue
Block a user