Add title, rating, o-counter to image lightbox (#2274)

This commit is contained in:
WithoutPants
2022-03-03 11:39:03 +11:00
committed by GitHub
parent d7473f4b38
commit 0737ca953d
8 changed files with 380 additions and 219 deletions

View File

@@ -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))

View File

@@ -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}

View File

@@ -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>

View File

@@ -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}

View File

@@ -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 = [

View File

@@ -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>
);
}; };

View File

@@ -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;

View File

@@ -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 {