Add gallery wall view, and new lightbox (#1008)

This commit is contained in:
InfiniteTF 2020-12-24 01:17:15 +01:00 committed by GitHub
parent c8bcaaf27d
commit 232a69c518
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 979 additions and 216 deletions

View file

@ -39,7 +39,6 @@
"flag-icon-css": "^3.5.0",
"flexbin": "^0.2.0",
"formik": "^2.2.1",
"fslightbox-react": "^1.5.0",
"graphql": "^15.4.0",
"graphql-tag": "^2.11.0",
"i18n-iso-countries": "^6.0.0",
@ -52,11 +51,9 @@
"react": "17.0.1",
"react-bootstrap": "1.4.0",
"react-dom": "17.0.1",
"react-images": "0.5.19",
"react-intl": "^5.8.8",
"react-jw-player": "1.19.1",
"react-markdown": "^5.0.2",
"react-photo-gallery": "^8.0.0",
"react-router-bootstrap": "^0.25.0",
"react-router-dom": "^5.2.0",
"react-router-hash-link": "^2.2.2",

View file

@ -2,6 +2,7 @@ import React from "react";
import { Route, Switch } from "react-router-dom";
import { IntlProvider } from "react-intl";
import { ToastProvider } from "src/hooks/Toast";
import LightboxProvider from "src/hooks/Lightbox/context";
import { library } from "@fortawesome/fontawesome-svg-core";
import { fas } from "@fortawesome/free-solid-svg-icons";
import "@formatjs/intl-numberformat/polyfill";
@ -53,25 +54,27 @@ export const App: React.FC = () => {
<ErrorBoundary>
<IntlProvider locale={language} messages={messages} formats={intlFormats}>
<ToastProvider>
<MainNavbar />
<div className="main container-fluid">
<Switch>
<Route exact path="/" component={Stats} />
<Route path="/scenes" component={Scenes} />
<Route path="/images" component={Images} />
<Route path="/galleries" component={Galleries} />
<Route path="/performers" component={Performers} />
<Route path="/tags" component={Tags} />
<Route path="/studios" component={Studios} />
<Route path="/movies" component={Movies} />
<Route path="/settings" component={Settings} />
<Route
path="/sceneFilenameParser"
component={SceneFilenameParser}
/>
<Route component={PageNotFound} />
</Switch>
</div>
<LightboxProvider>
<MainNavbar />
<div className="main container-fluid">
<Switch>
<Route exact path="/" component={Stats} />
<Route path="/scenes" component={Scenes} />
<Route path="/images" component={Images} />
<Route path="/galleries" component={Galleries} />
<Route path="/performers" component={Performers} />
<Route path="/tags" component={Tags} />
<Route path="/studios" component={Studios} />
<Route path="/movies" component={Movies} />
<Route path="/settings" component={Settings} />
<Route
path="/sceneFilenameParser"
component={SceneFilenameParser}
/>
<Route component={PageNotFound} />
</Switch>
</div>
</LightboxProvider>
</ToastProvider>
</IntlProvider>
</ErrorBoundary>

View file

@ -1,10 +1,12 @@
#### 💥 **Note: After upgrading, all scene file sizes will be 0B until a new [scan](/settings?tab=tasks) is run.
#### 💥 Note: After upgrading, all scene file sizes will be 0B until a new [scan](/settings?tab=tasks) is run.
### ✨ New Features
* Add gallery wall view.
* Add organized flag for scenes, galleries and images.
* Allow configuration of visible navbar items.
### 🎨 Improvements
* Pagination support and general improvements for image lightbox.
* Add mouse click support for CDP scrapers.
* Add gallery tabs to performer and studio pages.
* Add gallery scrapers to scraper page.

View file

@ -186,7 +186,6 @@ export const Gallery: React.FC = () => {
<Tab.Content>
<Tab.Pane eventKey="images">
{/* <GalleryViewer gallery={gallery} /> */}
<GalleryImagesPanel gallery={gallery} />
</Tab.Pane>
<Tab.Pane eventKey="add">

View file

@ -13,6 +13,7 @@ import { ListFilterModel } from "src/models/list-filter/filter";
import { DisplayMode } from "src/models/list-filter/types";
import { queryFindGalleries } from "src/core/StashService";
import { GalleryCard } from "./GalleryCard";
import GalleryWallCard from "./GalleryWallCard";
import { EditGalleriesDialog } from "./EditGalleriesDialog";
import { DeleteGalleriesDialog } from "./DeleteGalleriesDialog";
import { ExportDialog } from "../Shared/ExportDialog";
@ -212,7 +213,15 @@ export const GalleryList: React.FC<IGalleryList> = ({
);
}
if (filter.displayMode === DisplayMode.Wall) {
return <h1>TODO</h1>;
return (
<div className="row">
<div className="GalleryWall">
{result.data.findGalleries.galleries.map((gallery) => (
<GalleryWallCard key={gallery.id} gallery={gallery} />
))}
</div>
</div>
);
}
}

View file

@ -1,52 +1,36 @@
import React, { useState } from "react";
import React from "react";
import * as GQL from "src/core/generated-graphql";
import FsLightbox from "fslightbox-react";
import { useLightbox } from "src/hooks";
import "flexbin/flexbin.css";
interface IProps {
gallery: Partial<GQL.GalleryDataFragment>;
gallery: GQL.GalleryDataFragment;
}
export const GalleryViewer: React.FC<IProps> = ({ gallery }) => {
const [lightboxToggle, setLightboxToggle] = useState(false);
const [currentIndex, setCurrentIndex] = useState(0);
const images = gallery?.images ?? [];
const showLightbox = useLightbox({ images, showNavigation: false });
const openImage = (index: number) => {
setCurrentIndex(index);
setLightboxToggle(!lightboxToggle);
};
const photos = !gallery.images
? []
: gallery.images.map((file) => file.paths.image ?? "");
const thumbs = !gallery.images
? []
: gallery.images.map((file, index) => (
<div
role="link"
tabIndex={index}
key={file.checksum ?? index}
onClick={() => openImage(index)}
onKeyPress={() => openImage(index)}
>
<img
src={file.paths.thumbnail ?? ""}
loading="lazy"
className="gallery-image"
alt={file.title ?? index.toString()}
/>
</div>
));
const thumbs = images.map((file, index) => (
<div
role="link"
tabIndex={index}
key={file.checksum ?? index}
onClick={() => showLightbox(index)}
onKeyPress={() => showLightbox(index)}
>
<img
src={file.paths.thumbnail ?? ""}
loading="lazy"
className="gallery-image"
alt={file.title ?? index.toString()}
/>
</div>
));
return (
<div className="gallery">
<div className="flexbin">{thumbs}</div>
<FsLightbox
sourceIndex={currentIndex}
toggler={lightboxToggle}
sources={photos}
key={gallery.id!}
/>
</div>
);
};

View file

@ -0,0 +1,68 @@
import React from "react";
import { useIntl } from "react-intl";
import { Link } from "react-router-dom";
import * as GQL from "src/core/generated-graphql";
import { RatingStars, TruncatedText } from "src/components/Shared";
import { TextUtils } from "src/utils";
import { useGalleryLightbox } from "src/hooks";
const CLASSNAME = "GalleryWallCard";
const CLASSNAME_FOOTER = `${CLASSNAME}-footer`;
const CLASSNAME_IMG = `${CLASSNAME}-img`;
const CLASSNAME_TITLE = `${CLASSNAME}-title`;
interface IProps {
gallery: GQL.GallerySlimDataFragment;
}
const GalleryWallCard: React.FC<IProps> = ({ gallery }) => {
const intl = useIntl();
const showLightbox = useGalleryLightbox(gallery.id);
const orientation =
(gallery?.cover?.file.width ?? 0) > (gallery.cover?.file.height ?? 0)
? "landscape"
: "portrait";
const cover = gallery?.cover?.paths.thumbnail ?? "";
const title = gallery.title ?? gallery.path;
const performerNames = gallery.performers.map((p) => p.name);
const performers =
performerNames.length >= 2
? [...performerNames.slice(0, -2), performerNames.slice(-2).join(" & ")]
: performerNames;
return (
<>
<section
className={`${CLASSNAME} ${CLASSNAME}-${orientation}`}
onClick={showLightbox}
onKeyPress={showLightbox}
role="button"
tabIndex={0}
>
<RatingStars rating={gallery.rating} />
<img src={cover} alt="" className={CLASSNAME_IMG} />
<footer className={CLASSNAME_FOOTER}>
<Link
to={`/galleries/${gallery.id}`}
onClick={(e) => e.stopPropagation()}
>
{title && (
<TruncatedText
text={title}
lineCount={1}
className={CLASSNAME_TITLE}
/>
)}
<TruncatedText text={performers.join(", ")} />
<div>
{gallery.date && TextUtils.formatDate(intl, gallery.date)}
</div>
</Link>
</footer>
</section>
</>
);
};
export default GalleryWallCard;

View file

@ -96,3 +96,116 @@ $galleryTabWidth: 450px;
height: calc(1.5em + 0.75rem + 2px);
}
}
.GalleryWall {
display: flex;
flex-wrap: wrap;
margin: 0 auto;
width: 96vw;
/* Prevents last row from consuming all space and stretching images to oblivion */
&::after {
content: "";
flex: auto;
flex-grow: 9999;
}
}
.GalleryWallCard {
height: auto;
padding: 2px;
position: relative;
$width: 96vw;
&-landscape {
flex-grow: 2;
width: 96vw;
}
&-portrait {
flex-grow: 1;
width: 96vw;
}
@mixin galleryWidth($width) {
height: ($width / 3) * 2;
&-landscape {
width: $width;
}
&-portrait {
width: $width / 2;
}
}
@media (min-width: 576px) {
@include galleryWidth(96vw);
}
@media (min-width: 768px) {
@include galleryWidth(48vw);
}
@media (min-width: 1200px) {
@include galleryWidth(32vw);
}
&-img {
height: 100%;
object-fit: cover;
object-position: center 20%;
width: 100%;
}
&-title {
font-weight: bold;
}
&-footer {
background-image: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.3));
bottom: 0;
padding: 1rem;
position: absolute;
text-shadow: 1px 1px 3px black;
transition: 0s opacity;
width: 100%;
@media (min-width: 768px) {
opacity: 0;
}
&:hover {
.GalleryWallCard-title {
text-decoration: underline;
}
}
a {
color: white;
}
}
&:hover &-footer {
opacity: 1;
transition: 1s opacity;
transition-delay: 500ms;
a {
text-decoration: none;
}
}
.RatingStars {
position: absolute;
right: 1rem;
top: 1rem;
&-unfilled {
display: none;
}
&-filled {
filter: drop-shadow(1px 1px 1px #222);
}
}
}

View file

@ -1,7 +1,6 @@
import React, { useState } from "react";
import React, { useCallback, useState } from "react";
import _ from "lodash";
import { useHistory } from "react-router-dom";
import FsLightbox from "fslightbox-react";
import Mousetrap from "mousetrap";
import {
FindImagesQueryResult,
@ -9,7 +8,7 @@ import {
} from "src/core/generated-graphql";
import * as GQL from "src/core/generated-graphql";
import { queryFindImages } from "src/core/StashService";
import { useImagesList } from "src/hooks";
import { useImagesList, useLightbox } from "src/hooks";
import { TextUtils } from "src/utils";
import { ListFilterModel } from "src/models/list-filter/filter";
import { DisplayMode } from "src/models/list-filter/types";
@ -22,25 +21,45 @@ import { ExportDialog } from "../Shared/ExportDialog";
interface IImageWallProps {
images: GQL.SlimImageDataFragment[];
onChangePage: (page: number) => void;
currentPage: number;
pageCount: number;
}
const ImageWall: React.FC<IImageWallProps> = ({ images }) => {
const [lightboxToggle, setLightboxToggle] = useState(false);
const [currentIndex, setCurrentIndex] = useState(0);
const ImageWall: React.FC<IImageWallProps> = ({
images,
onChangePage,
currentPage,
pageCount,
}) => {
const handleLightBoxPage = useCallback(
(direction: number) => {
if (direction === -1) {
if (currentPage === 1) return false;
onChangePage(currentPage - 1);
} else {
if (currentPage === pageCount) return false;
onChangePage(currentPage + 1);
}
return direction === -1 || direction === 1;
},
[onChangePage, currentPage, pageCount]
);
const openImage = (index: number) => {
setCurrentIndex(index);
setLightboxToggle(!lightboxToggle);
};
const showLightbox = useLightbox({
images,
showNavigation: false,
pageCallback: handleLightBoxPage,
pageHeader: `Page ${currentPage} / ${pageCount}`,
});
const photos = images.map((image) => image.paths.image ?? "");
const thumbs = images.map((image, index) => (
<div
role="link"
tabIndex={index}
key={image.id}
onClick={() => openImage(index)}
onKeyPress={() => openImage(index)}
onClick={() => showLightbox(index)}
onKeyPress={() => showLightbox(index)}
>
<img
src={image.paths.thumbnail ?? ""}
@ -51,32 +70,9 @@ const ImageWall: React.FC<IImageWallProps> = ({ images }) => {
</div>
));
// FsLightbox doesn't update unless the key updates
const key = images.map((i) => i.id).join(",");
function onLightboxOpen() {
// disable mousetrap
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(Mousetrap as any).pause();
}
function onLightboxClose() {
// re-enable mousetrap
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(Mousetrap as any).unpause();
}
return (
<div className="gallery">
<div className="flexbin">{thumbs}</div>
<FsLightbox
sourceIndex={currentIndex}
toggler={lightboxToggle}
sources={photos}
key={key}
onOpen={onLightboxOpen}
onClose={onLightboxClose}
/>
</div>
);
};
@ -125,7 +121,7 @@ export const ImageList: React.FC<IImageList> = ({
};
};
const listData = useImagesList({
const { template, onSelectChange } = useImagesList({
zoomable: true,
selectable: true,
otherOperations,
@ -150,12 +146,7 @@ export const ImageList: React.FC<IImageList> = ({
filterCopy.itemsPerPage = 1;
filterCopy.currentPage = index + 1;
const singleResult = await queryFindImages(filterCopy);
if (
singleResult &&
singleResult.data &&
singleResult.data.findImages &&
singleResult.data.findImages.images.length === 1
) {
if (singleResult.data.findImages.images.length === 1) {
const { id } = singleResult!.data!.findImages!.images[0];
// navigate to the image player page
history.push(`/images/${id}`);
@ -228,7 +219,7 @@ export const ImageList: React.FC<IImageList> = ({
selecting={selectedIds.size > 0}
selected={selectedIds.has(image.id)}
onSelectedChanged={(selected: boolean, shiftKey: boolean) =>
listData.onSelectChange(image.id, selected, shiftKey)
onSelectChange(image.id, selected, shiftKey)
}
/>
);
@ -238,7 +229,9 @@ export const ImageList: React.FC<IImageList> = ({
result: FindImagesQueryResult,
filter: ListFilterModel,
selectedIds: Set<string>,
zoomIndex: number
zoomIndex: number,
onChangePage: (page: number) => void,
pageCount: number
) {
if (!result.data || !result.data.findImages) {
return;
@ -252,11 +245,15 @@ export const ImageList: React.FC<IImageList> = ({
</div>
);
}
// if (filter.displayMode === DisplayMode.List) {
// return <ImageListTable images={result.data.findImages.images} />;
// }
if (filter.displayMode === DisplayMode.Wall) {
return <ImageWall images={result.data.findImages.images} />;
return (
<ImageWall
images={result.data.findImages.images}
onChangePage={onChangePage}
currentPage={filter.currentPage}
pageCount={pageCount}
/>
);
}
}
@ -264,15 +261,24 @@ export const ImageList: React.FC<IImageList> = ({
result: FindImagesQueryResult,
filter: ListFilterModel,
selectedIds: Set<string>,
zoomIndex: number
zoomIndex: number,
onChangePage: (page: number) => void,
pageCount: number
) {
return (
<>
{maybeRenderImageExportDialog(selectedIds)}
{renderImages(result, filter, selectedIds, zoomIndex)}
{renderImages(
result,
filter,
selectedIds,
zoomIndex,
onChangePage,
pageCount
)}
</>
);
}
return listData.template;
return template;
};

View file

@ -16,9 +16,8 @@ import {
Icon,
LoadingIndicator,
} from "src/components/Shared";
import { useToast } from "src/hooks";
import { useLightbox, useToast } from "src/hooks";
import { TextUtils } from "src/utils";
import FsLightbox from "fslightbox-react";
import { PerformerDetailsPanel } from "./PerformerDetailsPanel";
import { PerformerOperationsPanel } from "./PerformerOperationsPanel";
import { PerformerScenesPanel } from "./PerformerScenesPanel";
@ -39,7 +38,6 @@ export const Performer: React.FC = () => {
// Performer state
const [imagePreview, setImagePreview] = useState<string | null>();
const [imageEncoding, setImageEncoding] = useState<boolean>(false);
const [lightboxToggle, setLightboxToggle] = useState(false);
const { data, loading: performerLoading, error } = useFindPerformer(id);
const performer = data?.findPerformer || ({} as Partial<GQL.Performer>);
@ -51,6 +49,10 @@ export const Performer: React.FC = () => {
? performer.image_path ?? ""
: imagePreview ?? `${performer.image_path}?default=true`;
const showLightbox = useLightbox({
images: [{ paths: { thumbnail: activeImage, image: activeImage } }],
});
// Network state
const [loading, setIsLoading] = useState(false);
const isLoading = performerLoading || loading;
@ -318,10 +320,7 @@ export const Performer: React.FC = () => {
{imageEncoding ? (
<LoadingIndicator message="Encoding image..." />
) : (
<Button
variant="link"
onClick={() => setLightboxToggle(!lightboxToggle)}
>
<Button variant="link" onClick={() => showLightbox()}>
<img className="performer" src={activeImage} alt="Performer" />
</Button>
)}
@ -342,7 +341,6 @@ export const Performer: React.FC = () => {
<div className="performer-tabs">{renderTabs()}</div>
</div>
</div>
<FsLightbox toggler={lightboxToggle} sources={[activeImage]} />
</div>
);
};

View file

@ -17,7 +17,7 @@ import { ErrorMessage, LoadingIndicator, Icon } from "src/components/Shared";
import { useToast } from "src/hooks";
import { ScenePlayer } from "src/components/ScenePlayer";
import { TextUtils, JWUtils } from "src/utils";
import * as Mousetrap from "mousetrap";
import Mousetrap from "mousetrap";
import { SceneMarkersPanel } from "./SceneMarkersPanel";
import { SceneFileInfoPanel } from "./SceneFileInfoPanel";
import { SceneEditPanel } from "./SceneEditPanel";

View file

@ -0,0 +1,35 @@
import React from "react";
import Icon from "./Icon";
const CLASSNAME = "RatingStars";
const CLASSNAME_FILLED = `${CLASSNAME}-filled`;
const CLASSNAME_UNFILLED = `${CLASSNAME}-unfilled`;
interface IProps {
rating?: number | null;
}
export const RatingStars: React.FC<IProps> = ({ rating }) =>
rating ? (
<div className={CLASSNAME}>
<Icon icon={["fas", "star"]} className={CLASSNAME_FILLED} />
<Icon
icon={[rating >= 2 ? "fas" : "far", "star"]}
className={rating >= 2 ? CLASSNAME_FILLED : CLASSNAME_UNFILLED}
/>
<Icon
icon={[rating >= 3 ? "fas" : "far", "star"]}
className={rating >= 3 ? CLASSNAME_FILLED : CLASSNAME_UNFILLED}
/>
<Icon
icon={[rating >= 4 ? "fas" : "far", "star"]}
className={rating >= 4 ? CLASSNAME_FILLED : CLASSNAME_UNFILLED}
/>
<Icon
icon={[rating === 5 ? "fas" : "far", "star"]}
className={rating === 5 ? CLASSNAME_FILLED : CLASSNAME_UNFILLED}
/>
</div>
) : (
<></>
);

View file

@ -23,3 +23,4 @@ export { default as SuccessIcon } from "./SuccessIcon";
export { default as ErrorMessage } from "./ErrorMessage";
export { default as TruncatedText } from "./TruncatedText";
export { BasicCard } from "./BasicCard";
export { RatingStars } from "./RatingStars";

View file

@ -182,3 +182,17 @@ button.collapse-button.btn-primary:not(:disabled):not(.disabled):active {
max-width: 300px;
}
}
.RatingStars {
&-unfilled {
path {
fill: white;
}
}
&-filled {
path {
fill: gold;
}
}
}

View file

@ -0,0 +1,350 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import * as GQL from "src/core/generated-graphql";
import { Button } from "react-bootstrap";
import cx from "classnames";
import Mousetrap from "mousetrap";
import { debounce } from "lodash";
import { Icon, LoadingIndicator } from "src/components/Shared";
const CLASSNAME = "Lightbox";
const CLASSNAME_HEADER = `${CLASSNAME}-header`;
const CLASSNAME_INDICATOR = `${CLASSNAME_HEADER}-indicator`;
const CLASSNAME_DISPLAY = `${CLASSNAME}-display`;
const CLASSNAME_CAROUSEL = `${CLASSNAME}-carousel`;
const CLASSNAME_INSTANT = `${CLASSNAME_CAROUSEL}-instant`;
const CLASSNAME_IMAGE = `${CLASSNAME_CAROUSEL}-image`;
const CLASSNAME_NAVBUTTON = `${CLASSNAME}-navbutton`;
const CLASSNAME_NAV = `${CLASSNAME}-nav`;
const CLASSNAME_NAVIMAGE = `${CLASSNAME_NAV}-image`;
const CLASSNAME_NAVSELECTED = `${CLASSNAME_NAV}-selected`;
type Image = Pick<GQL.Image, "paths">;
interface IProps {
images: Image[];
isVisible: boolean;
isLoading: boolean;
initialIndex?: number;
showNavigation: boolean;
pageHeader?: string;
pageCallback?: (direction: number) => boolean;
hide: () => void;
}
export const LightboxComponent: React.FC<IProps> = ({
images,
isVisible,
isLoading,
initialIndex = 0,
showNavigation,
pageHeader,
pageCallback,
hide,
}) => {
const index = useRef<number | null>(null);
const [instantTransition, setInstantTransition] = useState(false);
const [isSwitchingPage, setIsSwitchingPage] = useState(false);
const [isFullscreen, setFullscreen] = useState(false);
const containerRef = useRef<HTMLDivElement | null>(null);
const carouselRef = useRef<HTMLDivElement | null>(null);
const indicatorRef = useRef<HTMLDivElement | null>(null);
const navRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
setIsSwitchingPage(false);
if (index.current === -1) index.current = images.length - 1;
}, [images]);
const disableInstantTransition = debounce(
() => setInstantTransition(false),
400
);
const setInstant = useCallback(() => {
setInstantTransition(true);
disableInstantTransition();
}, [disableInstantTransition]);
const setIndex = useCallback(
(i: number) => {
if (images.length < 2) return;
index.current = i;
if (carouselRef.current) carouselRef.current.style.left = `${i * -100}vw`;
if (indicatorRef.current)
indicatorRef.current.innerHTML = `${i + 1} / ${images.length}`;
if (navRef.current) {
const currentThumb = navRef.current.children[i + 1];
if (currentThumb instanceof HTMLImageElement) {
const offset =
-1 *
(currentThumb.offsetLeft -
document.documentElement.clientWidth / 2);
navRef.current.style.left = `${offset}px`;
const previouslySelected = navRef.current.getElementsByClassName(
CLASSNAME_NAVSELECTED
)?.[0];
if (previouslySelected)
previouslySelected.className = CLASSNAME_NAVIMAGE;
currentThumb.className = `${CLASSNAME_NAVIMAGE} ${CLASSNAME_NAVSELECTED}`;
}
}
},
[images]
);
const selectIndex = (e: React.MouseEvent, i: number) => {
setIndex(i);
e.stopPropagation();
};
useEffect(() => {
if (isVisible) {
if (index.current === null) setIndex(initialIndex);
document.body.style.overflow = "hidden";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(Mousetrap as any).pause();
}
}, [initialIndex, isVisible, setIndex]);
const close = useCallback(() => {
if (!isFullscreen) {
hide();
document.body.style.overflow = "auto";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(Mousetrap as any).unpause();
} else document.exitFullscreen();
}, [isFullscreen, hide]);
const handleClose = (e: React.MouseEvent<HTMLDivElement>) => {
const { nodeName } = e.target as Node;
if (nodeName === "DIV" || nodeName === "PICTURE") close();
};
const handleLeft = useCallback(() => {
if (isSwitchingPage || index.current === -1) return;
if (index.current === 0) {
if (pageCallback) {
setIsSwitchingPage(true);
setIndex(-1);
// Check if calling page wants to swap page
const repage = pageCallback(-1);
if (!repage) {
setIsSwitchingPage(false);
setIndex(0);
}
} else setIndex(images.length - 1);
} else setIndex((index.current ?? 0) - 1);
}, [images, setIndex, pageCallback, isSwitchingPage]);
const handleRight = useCallback(() => {
if (isSwitchingPage) return;
if (index.current === images.length - 1) {
if (pageCallback) {
setIsSwitchingPage(true);
setIndex(0);
const repage = pageCallback?.(1);
if (!repage) {
setIsSwitchingPage(false);
setIndex(images.length - 1);
}
} else setIndex(0);
} else setIndex((index.current ?? 0) + 1);
}, [images, setIndex, pageCallback, isSwitchingPage]);
const handleKey = useCallback(
(e: KeyboardEvent) => {
if (e.repeat && (e.key === "ArrowRight" || e.key === "ArrowLeft"))
setInstant();
if (e.key === "ArrowLeft") handleLeft();
else if (e.key === "ArrowRight") handleRight();
else if (e.key === "Escape") close();
},
[setInstant, handleLeft, handleRight, close]
);
const handleFullScreenChange = () =>
setFullscreen(document.fullscreenElement !== null);
const handleTouchStart = (ev: React.TouchEvent<HTMLDivElement>) => {
setInstantTransition(true);
const el = ev.currentTarget;
if (ev.touches.length !== 1) return;
const startX = ev.touches[0].clientX;
let position = 0;
const resetPosition = () => {
if (carouselRef.current)
carouselRef.current.style.left = `${(index.current ?? 0) * -100}vw`;
};
const handleMove = (e: TouchEvent) => {
position = e.touches[0].clientX;
if (carouselRef.current)
carouselRef.current.style.left = `calc(${
(index.current ?? 0) * -100
}vw + ${e.touches[0].clientX - startX}px)`;
};
const handleEnd = () => {
const diff = position - startX;
if (diff <= -50) handleRight();
else if (diff >= 50) handleLeft();
else resetPosition();
// eslint-disable-next-line @typescript-eslint/no-use-before-define
cleanup();
};
const handleCancel = () => {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
cleanup();
resetPosition();
};
const cleanup = () => {
el.removeEventListener("touchmove", handleMove);
el.removeEventListener("touchend", handleEnd);
el.removeEventListener("touchcancel", handleCancel);
setInstantTransition(false);
};
el.addEventListener("touchmove", handleMove);
el.addEventListener("touchend", handleEnd);
el.addEventListener("touchcancel", handleCancel);
};
useEffect(() => {
if (isVisible) {
document.addEventListener("keydown", handleKey);
document.addEventListener("fullscreenchange", handleFullScreenChange);
}
return () => {
document.removeEventListener("keydown", handleKey);
document.removeEventListener("fullscreenchange", handleFullScreenChange);
};
}, [isVisible, handleKey]);
const toggleFullscreen = useCallback(() => {
if (!isFullscreen) containerRef.current?.requestFullscreen();
else document.exitFullscreen();
}, [isFullscreen]);
const navItems = images.map((image, i) => (
<img
src={image.paths.thumbnail ?? ""}
alt=""
className={cx(CLASSNAME_NAVIMAGE, {
[CLASSNAME_NAVSELECTED]: i === index.current,
})}
onClick={(e: React.MouseEvent) => selectIndex(e, i)}
role="presentation"
loading="lazy"
key={image.paths.thumbnail}
/>
));
const currentIndex = index.current === null ? initialIndex : index.current;
const element = isVisible ? (
<div
className={CLASSNAME}
role="presentation"
ref={containerRef}
onClick={handleClose}
>
{images.length > 0 && !isLoading && !isSwitchingPage ? (
<>
<div className={CLASSNAME_HEADER}>
<div className={CLASSNAME_INDICATOR}>
<span>{pageHeader}</span>
<b ref={indicatorRef}>
{`${currentIndex + 1} / ${images.length}`}
</b>
</div>
{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 className={CLASSNAME_DISPLAY} onTouchStart={handleTouchStart}>
{images.length > 1 && (
<Button
variant="link"
onClick={handleLeft}
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) => (
<div className={CLASSNAME_IMAGE} key={image.paths.image}>
<picture>
<source
srcSet={image.paths.image ?? ""}
media="(min-width: 800px)"
/>
<img src={image.paths.thumbnail ?? ""} alt="" />
</picture>
</div>
))}
</div>
{images.length > 1 && (
<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>
)}
</>
) : (
<LoadingIndicator />
)}
</div>
) : (
<></>
);
return element;
};

View file

@ -0,0 +1,54 @@
import React, { useCallback, useState } from "react";
import * as GQL from "src/core/generated-graphql";
import { LightboxComponent } from "./Lightbox";
type Image = Pick<GQL.Image, "paths">;
export interface IState {
images: Image[];
isVisible: boolean;
isLoading: boolean;
showNavigation: boolean;
initialIndex?: number;
pageCallback?: (direction: number) => boolean;
pageHeader?: string;
}
interface IContext {
setLightboxState: (state: Partial<IState>) => void;
}
export const LightboxContext = React.createContext<IContext>({
setLightboxState: () => {},
});
const Lightbox: React.FC = ({ children }) => {
const [lightboxState, setLightboxState] = useState<IState>({
images: [],
isVisible: false,
isLoading: false,
showNavigation: true,
});
const setPartialState = useCallback(
(state: Partial<IState>) => {
setLightboxState((currentState: IState) => ({
...currentState,
...state,
}));
},
[setLightboxState]
);
return (
<LightboxContext.Provider value={{ setLightboxState: setPartialState }}>
{children}
{lightboxState.isVisible && (
<LightboxComponent
{...lightboxState}
hide={() => setLightboxState({ ...lightboxState, isVisible: false })}
/>
)}
</LightboxContext.Provider>
);
};
export default Lightbox;

View file

@ -0,0 +1,73 @@
import { useCallback, useContext, useEffect } from "react";
import * as GQL from "src/core/generated-graphql";
import { LightboxContext, IState } from "./context";
export const useLightbox = (state: Partial<Omit<IState, "isVisible">>) => {
const { setLightboxState } = useContext(LightboxContext);
useEffect(() => {
setLightboxState({
images: state.images,
showNavigation: state.showNavigation,
pageCallback: state.pageCallback,
initialIndex: state.initialIndex,
pageHeader: state.pageHeader,
});
}, [
setLightboxState,
state.images,
state.showNavigation,
state.pageCallback,
state.initialIndex,
state.pageHeader,
]);
const show = useCallback(
(index?: number) => {
setLightboxState({
initialIndex: index,
isVisible: true,
});
},
[setLightboxState]
);
return show;
};
export const useGalleryLightbox = (id: string) => {
const { setLightboxState } = useContext(LightboxContext);
const [fetchGallery, { data }] = GQL.useFindGalleryLazyQuery({
variables: { id },
});
useEffect(() => {
if (data)
setLightboxState({
images: data.findGallery?.images ?? [],
isLoading: false,
isVisible: true,
});
}, [setLightboxState, data]);
const show = () => {
if (data)
setLightboxState({
isLoading: false,
isVisible: true,
images: data.findGallery?.images ?? [],
pageCallback: undefined,
pageHeader: undefined,
});
else {
setLightboxState({
isLoading: true,
isVisible: true,
pageCallback: undefined,
pageHeader: undefined,
});
fetchGallery();
}
};
return show;
};

View file

@ -0,0 +1,2 @@
export * from "./context";
export * from "./hooks";

View file

@ -0,0 +1,124 @@
.Lightbox {
background-color: rgba(20, 20, 20, 0.8);
bottom: 0;
display: flex;
flex-direction: column;
left: 0;
position: fixed;
right: 0;
top: 0;
z-index: 1040;
.fa-icon {
path {
fill: white;
}
opacity: 0.4;
&:hover {
opacity: 1;
}
}
&-header {
align-items: center;
display: flex;
flex-shrink: 0;
height: 4rem;
&-indicator {
display: flex;
flex-direction: column;
margin-left: 49%;
margin-right: auto;
text-align: center;
}
.fa-icon {
height: 1.5rem;
opacity: 1;
width: 1.5rem;
}
}
&-display {
display: flex;
height: 100%;
justify-content: space-between;
margin-bottom: 2rem;
position: relative;
}
&-carousel {
display: flex;
height: 100%;
position: absolute;
transition: left 400ms;
&-instant {
transition-duration: 0ms;
}
&-image {
content-visibility: auto;
display: flex;
height: 100%;
width: 100vw;
picture {
display: flex;
height: 100%;
margin: auto;
}
img {
margin: auto;
max-height: 100%;
max-width: 100%;
object-fit: contain;
}
}
}
&-navbutton {
z-index: 1045;
.fa-icon {
height: 4rem;
width: 4rem;
}
&:focus {
box-shadow: none;
}
&:hover {
filter: drop-shadow(2px 2px 2px black);
}
}
&-nav {
display: flex;
flex-direction: row;
flex-shrink: 0;
height: 10rem;
margin: 0 auto 2rem 0;
padding: 0 10rem;
position: relative;
transition: left 400ms;
@media (max-height: 800px) {
display: none;
}
&-selected {
box-shadow: 0 0 0 6px white;
}
&-image {
cursor: pointer;
height: 100%;
margin-right: 1rem;
}
}
}

View file

@ -64,6 +64,7 @@ interface IListHookData {
filter: ListFilterModel;
template: React.ReactElement;
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void;
onChangePage: (page: number) => void;
}
export interface IListHookOperation<T> {
@ -92,7 +93,9 @@ interface IListHookOptions<T, E> {
result: T,
filter: ListFilterModel,
selectedIds: Set<string>,
zoomIndex: number
zoomIndex: number,
onChangePage: (page: number) => void,
pageCount: number
) => React.ReactNode;
renderEditDialog?: (
selected: E[],
@ -350,10 +353,19 @@ const RenderList = <
return;
}
const pages = Math.ceil(totalCount / filter.itemsPerPage);
return (
<>
{renderPagination()}
{renderContent(result, filter, selectedIds, zoomIndex)}
{renderContent(
result,
filter,
selectedIds,
zoomIndex,
onChangePage,
pages
)}
<PaginationIndex
itemsPerPage={filter.itemsPerPage}
currentPage={filter.currentPage}
@ -525,6 +537,7 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
filter,
template,
onSelectChange,
onChangePage,
};
};

View file

@ -8,3 +8,4 @@ export {
useStudiosList,
usePerformersList,
} from "./ListHook";
export { useLightbox, useGalleryLightbox } from "./Lightbox";

View file

@ -18,6 +18,7 @@
@import "src/components/Wall/styles.scss";
@import "../node_modules/flag-icon-css/css/flag-icon.min.css";
@import "src/components/Tagger/styles.scss";
@import "src/hooks/Lightbox/lightbox.scss";
/* stylelint-disable */
#root {

View file

@ -248,6 +248,11 @@ export class ListFilterModel {
new PerformersCriterionOption(),
new StudiosCriterionOption(),
];
this.displayModeOptions = [
DisplayMode.Grid,
DisplayMode.List,
DisplayMode.Wall,
];
break;
case FilterMode.SceneMarkers:
this.sortBy = "title";

View file

@ -3281,14 +3281,6 @@ anymatch@^3.0.3, anymatch@~3.1.1:
normalize-path "^3.0.0"
picomatch "^2.0.4"
aphrodite@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/aphrodite/-/aphrodite-0.5.0.tgz#a4b9a8902662395d2702e70ac7a2b4ca66f25703"
integrity sha1-pLmokCZiOV0nAucKx6K0ymbyVwM=
dependencies:
asap "^2.0.3"
inline-style-prefixer "^2.0.0"
apollo-upload-client@^14.1.2:
version "14.1.2"
resolved "https://registry.yarnpkg.com/apollo-upload-client/-/apollo-upload-client-14.1.2.tgz#7a72b000f1cd67eaf8f12b4bda2796d0898c0dae"
@ -3419,7 +3411,7 @@ arrify@^2.0.1:
resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==
asap@^2.0.3, asap@~2.0.3, asap@~2.0.6:
asap@~2.0.3, asap@~2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
@ -3948,11 +3940,6 @@ bootstrap@^4.5.3:
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.5.3.tgz#c6a72b355aaf323920be800246a6e4ef30997fe6"
integrity sha512-o9ppKQioXGqhw8Z7mah6KdTYpNQY//tipnkxppWhPbiSWdD+1raYsnhwEZjkTHYbGee4cVQ0Rx65EhOY/HNLcQ==
bowser@^1.0.0:
version "1.9.4"
resolved "https://registry.yarnpkg.com/bowser/-/bowser-1.9.4.tgz#890c58a2813a9d3243704334fa81b96a5c150c9a"
integrity sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ==
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@ -5544,13 +5531,6 @@ dom-converter@^0.2:
dependencies:
utila "~0.4"
dom-helpers@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8"
integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==
dependencies:
"@babel/runtime" "^7.1.2"
dom-helpers@^5.0.1, dom-helpers@^5.1.2, dom-helpers@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.0.tgz#57fd054c5f8f34c52a3eeffdb7e7e93cd357d95b"
@ -6285,11 +6265,6 @@ execall@^2.0.0:
dependencies:
clone-regexp "^2.1.0"
exenv@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d"
integrity sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=
exif-parser@^0.1.12:
version "0.1.12"
resolved "https://registry.yarnpkg.com/exif-parser/-/exif-parser-0.1.12.tgz#58a9d2d72c02c1f6f02a0ef4a9166272b7760922"
@ -6873,11 +6848,6 @@ fsevents@^2.1.2, fsevents@^2.1.3, fsevents@~2.1.2:
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e"
integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==
fslightbox-react@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/fslightbox-react/-/fslightbox-react-1.5.0.tgz#07cf41d7ff8b02a79a0886d13519550b79dc50e5"
integrity sha512-xBe1K06pa3opWar/xBtArsHMnxMJWsmg5EmNdDtheDL9nMCqk2AXYlNnstfYVqtJJjqNReqeL21wc52Yy4rwWg==
fstream@^1.0.0, fstream@^1.0.12:
version "1.0.12"
resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045"
@ -7541,11 +7511,6 @@ human-signals@^1.1.1:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
hyphenate-style-name@^1.0.1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d"
integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==
i18n-iso-countries@^6.0.0:
version "6.2.2"
resolved "https://registry.yarnpkg.com/i18n-iso-countries/-/i18n-iso-countries-6.2.2.tgz#6b63d00e90ee4022e8c159a9e688d2a8156b0e0b"
@ -7744,14 +7709,6 @@ ini@^1.3.5, ini@~1.3.0:
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
inline-style-prefixer@^2.0.0:
version "2.0.5"
resolved "https://registry.yarnpkg.com/inline-style-prefixer/-/inline-style-prefixer-2.0.5.tgz#c153c7e88fd84fef5c602e95a8168b2770671fe7"
integrity sha1-wVPH6I/YT+9cYC6VqBaLJ3BnH+c=
dependencies:
bowser "^1.0.0"
hyphenate-style-name "^1.0.1"
inquirer@7.3.3, inquirer@^7.3.3:
version "7.3.3"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003"
@ -11903,7 +11860,7 @@ prop-types-extra@^1.1.0:
react-is "^16.3.2"
warning "^4.0.0"
prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@~15.7.2:
prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@ -12176,16 +12133,6 @@ react-fast-compare@^2.0.1:
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
react-images@0.5.19:
version "0.5.19"
resolved "https://registry.yarnpkg.com/react-images/-/react-images-0.5.19.tgz#9339570029e065f9f28a19f03fdb5d9d5aa109d3"
integrity sha512-B3d4W1uFJj+m17K8S65iAyEJShKGBjPk7n7N1YsPiAydEm8mIq9a6CoeQFMY1d7N2QMs6FBCjT9vELyc5jP5JA==
dependencies:
aphrodite "^0.5.0"
prop-types "^15.6.0"
react-scrolllock "^2.0.1"
react-transition-group "2"
react-input-autosize@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.2.tgz#fcaa7020568ec206bc04be36f4eb68e647c4d8c2"
@ -12263,19 +12210,6 @@ react-overlays@^4.1.0:
uncontrollable "^7.0.0"
warning "^4.0.3"
react-photo-gallery@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/react-photo-gallery/-/react-photo-gallery-8.0.0.tgz#04ff9f902a2342660e63e6817b4f010488db02b8"
integrity sha512-Y9458yygEB9cIZAWlBWuenlR+ghin1RopmmU3Vice8BeJl0Se7hzfxGDq8W1armB/ic/kphGg+G1jq5fOEd0sw==
dependencies:
prop-types "~15.7.2"
resize-observer-polyfill "^1.5.0"
react-prop-toggle@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/react-prop-toggle/-/react-prop-toggle-1.0.2.tgz#8b0b7e74653606b1427cfcf6c4eaa9198330568e"
integrity sha512-JmerjAXs7qJ959+d0Ygt7Cb2+4fG+n3I2VXO6JO0AcAY1vkRN/JpZKAN67CMXY889xEJcfylmMPhzvf6nWO68Q==
react-refresh@^0.8.3:
version "0.8.3"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
@ -12389,14 +12323,6 @@ react-scripts@^4.0.0:
optionalDependencies:
fsevents "^2.1.3"
react-scrolllock@^2.0.1:
version "2.0.7"
resolved "https://registry.yarnpkg.com/react-scrolllock/-/react-scrolllock-2.0.7.tgz#3b879e1fe308fc900ab76e226e9be594c41226fd"
integrity sha512-Gzpu8+ulxdYcybAgJOFTXc70xs7SBZDQbZNpKzchZUgLCJKjz6lrgESx6LHHZgfELx1xYL4yHu3kYQGQPFas/g==
dependencies:
exenv "^1.2.2"
react-prop-toggle "^1.0.2"
react-select@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/react-select/-/react-select-3.1.0.tgz#ab098720b2e9fe275047c993f0d0caf5ded17c27"
@ -12411,16 +12337,6 @@ react-select@^3.1.0:
react-input-autosize "^2.2.2"
react-transition-group "^4.3.0"
react-transition-group@2:
version "2.9.0"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d"
integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==
dependencies:
dom-helpers "^3.4.0"
loose-envify "^1.4.0"
prop-types "^15.6.2"
react-lifecycles-compat "^3.0.4"
react-transition-group@^4.3.0, react-transition-group@^4.4.1:
version "4.4.1"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9"
@ -12865,11 +12781,6 @@ requires-port@^1.0.0:
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
resize-observer-polyfill@^1.5.0:
version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
resolve-cwd@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"