From 47b1be1e50a702164c9b00ef9c319a4595f5d275 Mon Sep 17 00:00:00 2001 From: InfiniteTF Date: Thu, 24 Dec 2020 01:17:15 +0100 Subject: [PATCH] Add gallery wall view, and new lightbox (#1008) --- ui/v2.5/package.json | 3 - ui/v2.5/src/App.tsx | 41 +- .../src/components/Changelog/versions/v050.md | 4 +- .../Galleries/GalleryDetails/Gallery.tsx | 1 - .../src/components/Galleries/GalleryList.tsx | 11 +- .../components/Galleries/GalleryViewer.tsx | 58 ++- .../components/Galleries/GalleryWallCard.tsx | 68 ++++ ui/v2.5/src/components/Galleries/styles.scss | 113 ++++++ ui/v2.5/src/components/Images/ImageList.tsx | 110 +++--- .../Performers/PerformerDetails/Performer.tsx | 14 +- .../components/Scenes/SceneDetails/Scene.tsx | 2 +- ui/v2.5/src/components/Shared/RatingStars.tsx | 35 ++ ui/v2.5/src/components/Shared/index.ts | 1 + ui/v2.5/src/components/Shared/styles.scss | 14 + ui/v2.5/src/hooks/Lightbox/Lightbox.tsx | 350 ++++++++++++++++++ ui/v2.5/src/hooks/Lightbox/context.tsx | 54 +++ ui/v2.5/src/hooks/Lightbox/hooks.ts | 73 ++++ ui/v2.5/src/hooks/Lightbox/index.ts | 2 + ui/v2.5/src/hooks/Lightbox/lightbox.scss | 124 +++++++ ui/v2.5/src/hooks/ListHook.tsx | 17 +- ui/v2.5/src/hooks/index.ts | 1 + ui/v2.5/src/index.scss | 1 + ui/v2.5/src/models/list-filter/filter.ts | 5 + ui/v2.5/yarn.lock | 93 +---- 24 files changed, 979 insertions(+), 216 deletions(-) create mode 100644 ui/v2.5/src/components/Galleries/GalleryWallCard.tsx create mode 100644 ui/v2.5/src/components/Shared/RatingStars.tsx create mode 100644 ui/v2.5/src/hooks/Lightbox/Lightbox.tsx create mode 100644 ui/v2.5/src/hooks/Lightbox/context.tsx create mode 100644 ui/v2.5/src/hooks/Lightbox/hooks.ts create mode 100644 ui/v2.5/src/hooks/Lightbox/index.ts create mode 100644 ui/v2.5/src/hooks/Lightbox/lightbox.scss diff --git a/ui/v2.5/package.json b/ui/v2.5/package.json index 716609f0fdf..e4b0a13f6b4 100644 --- a/ui/v2.5/package.json +++ b/ui/v2.5/package.json @@ -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", diff --git a/ui/v2.5/src/App.tsx b/ui/v2.5/src/App.tsx index 29f2dda7748..9fcb99f6672 100755 --- a/ui/v2.5/src/App.tsx +++ b/ui/v2.5/src/App.tsx @@ -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 = () => { - -
- - - - - - - - - - - - - -
+ + +
+ + + + + + + + + + + + + +
+
diff --git a/ui/v2.5/src/components/Changelog/versions/v050.md b/ui/v2.5/src/components/Changelog/versions/v050.md index 430e95bb772..f79c0a8723a 100644 --- a/ui/v2.5/src/components/Changelog/versions/v050.md +++ b/ui/v2.5/src/components/Changelog/versions/v050.md @@ -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. diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx index 9bd621dc944..e41aa0fd53d 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx @@ -186,7 +186,6 @@ export const Gallery: React.FC = () => { - {/* */} diff --git a/ui/v2.5/src/components/Galleries/GalleryList.tsx b/ui/v2.5/src/components/Galleries/GalleryList.tsx index b5d7fe66334..283d64e1017 100644 --- a/ui/v2.5/src/components/Galleries/GalleryList.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryList.tsx @@ -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 = ({ ); } if (filter.displayMode === DisplayMode.Wall) { - return

TODO

; + return ( +
+
+ {result.data.findGalleries.galleries.map((gallery) => ( + + ))} +
+
+ ); } } diff --git a/ui/v2.5/src/components/Galleries/GalleryViewer.tsx b/ui/v2.5/src/components/Galleries/GalleryViewer.tsx index 0865f873574..5ed9f42f105 100644 --- a/ui/v2.5/src/components/Galleries/GalleryViewer.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryViewer.tsx @@ -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; + gallery: GQL.GalleryDataFragment; } export const GalleryViewer: React.FC = ({ 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) => ( -
openImage(index)} - onKeyPress={() => openImage(index)} - > - {file.title -
- )); + const thumbs = images.map((file, index) => ( +
showLightbox(index)} + onKeyPress={() => showLightbox(index)} + > + {file.title +
+ )); return (
{thumbs}
-
); }; diff --git a/ui/v2.5/src/components/Galleries/GalleryWallCard.tsx b/ui/v2.5/src/components/Galleries/GalleryWallCard.tsx new file mode 100644 index 00000000000..376f1062c5f --- /dev/null +++ b/ui/v2.5/src/components/Galleries/GalleryWallCard.tsx @@ -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 = ({ 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 ( + <> +
+ + +
+ e.stopPropagation()} + > + {title && ( + + )} + +
+ {gallery.date && TextUtils.formatDate(intl, gallery.date)} +
+ +
+
+ + ); +}; + +export default GalleryWallCard; diff --git a/ui/v2.5/src/components/Galleries/styles.scss b/ui/v2.5/src/components/Galleries/styles.scss index 15e4c040a5c..943e538dae8 100644 --- a/ui/v2.5/src/components/Galleries/styles.scss +++ b/ui/v2.5/src/components/Galleries/styles.scss @@ -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); + } + } +} diff --git a/ui/v2.5/src/components/Images/ImageList.tsx b/ui/v2.5/src/components/Images/ImageList.tsx index 394b8c03ee3..48cdb9824fd 100644 --- a/ui/v2.5/src/components/Images/ImageList.tsx +++ b/ui/v2.5/src/components/Images/ImageList.tsx @@ -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 = ({ images }) => { - const [lightboxToggle, setLightboxToggle] = useState(false); - const [currentIndex, setCurrentIndex] = useState(0); +const ImageWall: React.FC = ({ + 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) => (
openImage(index)} - onKeyPress={() => openImage(index)} + onClick={() => showLightbox(index)} + onKeyPress={() => showLightbox(index)} > = ({ images }) => {
)); - // 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 (
{thumbs}
-
); }; @@ -125,7 +121,7 @@ export const ImageList: React.FC = ({ }; }; - const listData = useImagesList({ + const { template, onSelectChange } = useImagesList({ zoomable: true, selectable: true, otherOperations, @@ -150,12 +146,7 @@ export const ImageList: React.FC = ({ 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 = ({ 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 = ({ result: FindImagesQueryResult, filter: ListFilterModel, selectedIds: Set, - 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 = ({ ); } - // if (filter.displayMode === DisplayMode.List) { - // return ; - // } if (filter.displayMode === DisplayMode.Wall) { - return ; + return ( + + ); } } @@ -264,15 +261,24 @@ export const ImageList: React.FC = ({ result: FindImagesQueryResult, filter: ListFilterModel, selectedIds: Set, - 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; }; diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx index 5fe01d9efc3..771f43804bb 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx @@ -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(); const [imageEncoding, setImageEncoding] = useState(false); - const [lightboxToggle, setLightboxToggle] = useState(false); const { data, loading: performerLoading, error } = useFindPerformer(id); const performer = data?.findPerformer || ({} as Partial); @@ -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 ? ( ) : ( - )} @@ -342,7 +341,6 @@ export const Performer: React.FC = () => {
{renderTabs()}
- ); }; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx index 4f1ca2d66d3..149fa6fd35b 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx @@ -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"; diff --git a/ui/v2.5/src/components/Shared/RatingStars.tsx b/ui/v2.5/src/components/Shared/RatingStars.tsx new file mode 100644 index 00000000000..0847dba0bed --- /dev/null +++ b/ui/v2.5/src/components/Shared/RatingStars.tsx @@ -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 = ({ rating }) => + rating ? ( +
+ + = 2 ? "fas" : "far", "star"]} + className={rating >= 2 ? CLASSNAME_FILLED : CLASSNAME_UNFILLED} + /> + = 3 ? "fas" : "far", "star"]} + className={rating >= 3 ? CLASSNAME_FILLED : CLASSNAME_UNFILLED} + /> + = 4 ? "fas" : "far", "star"]} + className={rating >= 4 ? CLASSNAME_FILLED : CLASSNAME_UNFILLED} + /> + +
+ ) : ( + <> + ); diff --git a/ui/v2.5/src/components/Shared/index.ts b/ui/v2.5/src/components/Shared/index.ts index e299a1eb06e..6395cf0aea8 100644 --- a/ui/v2.5/src/components/Shared/index.ts +++ b/ui/v2.5/src/components/Shared/index.ts @@ -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"; diff --git a/ui/v2.5/src/components/Shared/styles.scss b/ui/v2.5/src/components/Shared/styles.scss index e99b87f8212..02bacdef5a4 100644 --- a/ui/v2.5/src/components/Shared/styles.scss +++ b/ui/v2.5/src/components/Shared/styles.scss @@ -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; + } + } +} diff --git a/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx b/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx new file mode 100644 index 00000000000..43e429935fd --- /dev/null +++ b/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx @@ -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; +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 = ({ + images, + isVisible, + isLoading, + initialIndex = 0, + showNavigation, + pageHeader, + pageCallback, + hide, +}) => { + const index = useRef(null); + const [instantTransition, setInstantTransition] = useState(false); + const [isSwitchingPage, setIsSwitchingPage] = useState(false); + const [isFullscreen, setFullscreen] = useState(false); + const containerRef = useRef(null); + const carouselRef = useRef(null); + const indicatorRef = useRef(null); + const navRef = useRef(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) => { + 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) => { + 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) => ( + selectIndex(e, i)} + role="presentation" + loading="lazy" + key={image.paths.thumbnail} + /> + )); + + const currentIndex = index.current === null ? initialIndex : index.current; + + const element = isVisible ? ( +
+ {images.length > 0 && !isLoading && !isSwitchingPage ? ( + <> +
+
+ {pageHeader} + + {`${currentIndex + 1} / ${images.length}`} + +
+ {document.fullscreenEnabled && ( + + )} + +
+
+ {images.length > 1 && ( + + )} + +
+ {images.map((image) => ( +
+ + + + +
+ ))} +
+ + {images.length > 1 && ( + + )} +
+ {showNavigation && !isFullscreen && images.length > 1 && ( +
+ + {navItems} + +
+ )} + + ) : ( + + )} +
+ ) : ( + <> + ); + + return element; +}; diff --git a/ui/v2.5/src/hooks/Lightbox/context.tsx b/ui/v2.5/src/hooks/Lightbox/context.tsx new file mode 100644 index 00000000000..cff2e2e288e --- /dev/null +++ b/ui/v2.5/src/hooks/Lightbox/context.tsx @@ -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; + +export interface IState { + images: Image[]; + isVisible: boolean; + isLoading: boolean; + showNavigation: boolean; + initialIndex?: number; + pageCallback?: (direction: number) => boolean; + pageHeader?: string; +} +interface IContext { + setLightboxState: (state: Partial) => void; +} + +export const LightboxContext = React.createContext({ + setLightboxState: () => {}, +}); +const Lightbox: React.FC = ({ children }) => { + const [lightboxState, setLightboxState] = useState({ + images: [], + isVisible: false, + isLoading: false, + showNavigation: true, + }); + + const setPartialState = useCallback( + (state: Partial) => { + setLightboxState((currentState: IState) => ({ + ...currentState, + ...state, + })); + }, + [setLightboxState] + ); + + return ( + + {children} + {lightboxState.isVisible && ( + setLightboxState({ ...lightboxState, isVisible: false })} + /> + )} + + ); +}; + +export default Lightbox; diff --git a/ui/v2.5/src/hooks/Lightbox/hooks.ts b/ui/v2.5/src/hooks/Lightbox/hooks.ts new file mode 100644 index 00000000000..fa5685e1752 --- /dev/null +++ b/ui/v2.5/src/hooks/Lightbox/hooks.ts @@ -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>) => { + 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; +}; diff --git a/ui/v2.5/src/hooks/Lightbox/index.ts b/ui/v2.5/src/hooks/Lightbox/index.ts new file mode 100644 index 00000000000..493207ae702 --- /dev/null +++ b/ui/v2.5/src/hooks/Lightbox/index.ts @@ -0,0 +1,2 @@ +export * from "./context"; +export * from "./hooks"; diff --git a/ui/v2.5/src/hooks/Lightbox/lightbox.scss b/ui/v2.5/src/hooks/Lightbox/lightbox.scss new file mode 100644 index 00000000000..895c7bd80f4 --- /dev/null +++ b/ui/v2.5/src/hooks/Lightbox/lightbox.scss @@ -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; + } + } +} diff --git a/ui/v2.5/src/hooks/ListHook.tsx b/ui/v2.5/src/hooks/ListHook.tsx index 2f83b8966e3..e358d11140b 100644 --- a/ui/v2.5/src/hooks/ListHook.tsx +++ b/ui/v2.5/src/hooks/ListHook.tsx @@ -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 { @@ -92,7 +93,9 @@ interface IListHookOptions { result: T, filter: ListFilterModel, selectedIds: Set, - 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 + )} ( filter, template, onSelectChange, + onChangePage, }; }; diff --git a/ui/v2.5/src/hooks/index.ts b/ui/v2.5/src/hooks/index.ts index 2afa5efc4d9..15ddf6fea65 100644 --- a/ui/v2.5/src/hooks/index.ts +++ b/ui/v2.5/src/hooks/index.ts @@ -8,3 +8,4 @@ export { useStudiosList, usePerformersList, } from "./ListHook"; +export { useLightbox, useGalleryLightbox } from "./Lightbox"; diff --git a/ui/v2.5/src/index.scss b/ui/v2.5/src/index.scss index 17ee99fde22..5270b858c0e 100755 --- a/ui/v2.5/src/index.scss +++ b/ui/v2.5/src/index.scss @@ -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 { diff --git a/ui/v2.5/src/models/list-filter/filter.ts b/ui/v2.5/src/models/list-filter/filter.ts index ae58e37b88f..4d6bb3e6518 100644 --- a/ui/v2.5/src/models/list-filter/filter.ts +++ b/ui/v2.5/src/models/list-filter/filter.ts @@ -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"; diff --git a/ui/v2.5/yarn.lock b/ui/v2.5/yarn.lock index bd9b92ccf02..04153c904e5 100644 --- a/ui/v2.5/yarn.lock +++ b/ui/v2.5/yarn.lock @@ -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"