mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Add gallery wall view, and new lightbox (#1008)
This commit is contained in:
@@ -39,7 +39,6 @@
|
|||||||
"flag-icon-css": "^3.5.0",
|
"flag-icon-css": "^3.5.0",
|
||||||
"flexbin": "^0.2.0",
|
"flexbin": "^0.2.0",
|
||||||
"formik": "^2.2.1",
|
"formik": "^2.2.1",
|
||||||
"fslightbox-react": "^1.5.0",
|
|
||||||
"graphql": "^15.4.0",
|
"graphql": "^15.4.0",
|
||||||
"graphql-tag": "^2.11.0",
|
"graphql-tag": "^2.11.0",
|
||||||
"i18n-iso-countries": "^6.0.0",
|
"i18n-iso-countries": "^6.0.0",
|
||||||
@@ -52,11 +51,9 @@
|
|||||||
"react": "17.0.1",
|
"react": "17.0.1",
|
||||||
"react-bootstrap": "1.4.0",
|
"react-bootstrap": "1.4.0",
|
||||||
"react-dom": "17.0.1",
|
"react-dom": "17.0.1",
|
||||||
"react-images": "0.5.19",
|
|
||||||
"react-intl": "^5.8.8",
|
"react-intl": "^5.8.8",
|
||||||
"react-jw-player": "1.19.1",
|
"react-jw-player": "1.19.1",
|
||||||
"react-markdown": "^5.0.2",
|
"react-markdown": "^5.0.2",
|
||||||
"react-photo-gallery": "^8.0.0",
|
|
||||||
"react-router-bootstrap": "^0.25.0",
|
"react-router-bootstrap": "^0.25.0",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-router-hash-link": "^2.2.2",
|
"react-router-hash-link": "^2.2.2",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from "react";
|
|||||||
import { Route, Switch } from "react-router-dom";
|
import { Route, Switch } from "react-router-dom";
|
||||||
import { IntlProvider } from "react-intl";
|
import { IntlProvider } from "react-intl";
|
||||||
import { ToastProvider } from "src/hooks/Toast";
|
import { ToastProvider } from "src/hooks/Toast";
|
||||||
|
import LightboxProvider from "src/hooks/Lightbox/context";
|
||||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||||
import { fas } from "@fortawesome/free-solid-svg-icons";
|
import { fas } from "@fortawesome/free-solid-svg-icons";
|
||||||
import "@formatjs/intl-numberformat/polyfill";
|
import "@formatjs/intl-numberformat/polyfill";
|
||||||
@@ -53,6 +54,7 @@ export const App: React.FC = () => {
|
|||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<IntlProvider locale={language} messages={messages} formats={intlFormats}>
|
<IntlProvider locale={language} messages={messages} formats={intlFormats}>
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
|
<LightboxProvider>
|
||||||
<MainNavbar />
|
<MainNavbar />
|
||||||
<div className="main container-fluid">
|
<div className="main container-fluid">
|
||||||
<Switch>
|
<Switch>
|
||||||
@@ -72,6 +74,7 @@ export const App: React.FC = () => {
|
|||||||
<Route component={PageNotFound} />
|
<Route component={PageNotFound} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
|
</LightboxProvider>
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|||||||
@@ -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
|
### ✨ New Features
|
||||||
|
* Add gallery wall view.
|
||||||
* Add organized flag for scenes, galleries and images.
|
* Add organized flag for scenes, galleries and images.
|
||||||
* Allow configuration of visible navbar items.
|
* Allow configuration of visible navbar items.
|
||||||
|
|
||||||
### 🎨 Improvements
|
### 🎨 Improvements
|
||||||
|
* Pagination support and general improvements for image lightbox.
|
||||||
* Add mouse click support for CDP scrapers.
|
* Add mouse click support for CDP scrapers.
|
||||||
* Add gallery tabs to performer and studio pages.
|
* Add gallery tabs to performer and studio pages.
|
||||||
* Add gallery scrapers to scraper page.
|
* Add gallery scrapers to scraper page.
|
||||||
|
|||||||
@@ -186,7 +186,6 @@ export const Gallery: React.FC = () => {
|
|||||||
|
|
||||||
<Tab.Content>
|
<Tab.Content>
|
||||||
<Tab.Pane eventKey="images">
|
<Tab.Pane eventKey="images">
|
||||||
{/* <GalleryViewer gallery={gallery} /> */}
|
|
||||||
<GalleryImagesPanel gallery={gallery} />
|
<GalleryImagesPanel gallery={gallery} />
|
||||||
</Tab.Pane>
|
</Tab.Pane>
|
||||||
<Tab.Pane eventKey="add">
|
<Tab.Pane eventKey="add">
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { ListFilterModel } from "src/models/list-filter/filter";
|
|||||||
import { DisplayMode } from "src/models/list-filter/types";
|
import { DisplayMode } from "src/models/list-filter/types";
|
||||||
import { queryFindGalleries } from "src/core/StashService";
|
import { queryFindGalleries } from "src/core/StashService";
|
||||||
import { GalleryCard } from "./GalleryCard";
|
import { GalleryCard } from "./GalleryCard";
|
||||||
|
import GalleryWallCard from "./GalleryWallCard";
|
||||||
import { EditGalleriesDialog } from "./EditGalleriesDialog";
|
import { EditGalleriesDialog } from "./EditGalleriesDialog";
|
||||||
import { DeleteGalleriesDialog } from "./DeleteGalleriesDialog";
|
import { DeleteGalleriesDialog } from "./DeleteGalleriesDialog";
|
||||||
import { ExportDialog } from "../Shared/ExportDialog";
|
import { ExportDialog } from "../Shared/ExportDialog";
|
||||||
@@ -212,7 +213,15 @@ export const GalleryList: React.FC<IGalleryList> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (filter.displayMode === DisplayMode.Wall) {
|
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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +1,23 @@
|
|||||||
import React, { useState } from "react";
|
import React from "react";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import FsLightbox from "fslightbox-react";
|
import { useLightbox } from "src/hooks";
|
||||||
import "flexbin/flexbin.css";
|
import "flexbin/flexbin.css";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
gallery: Partial<GQL.GalleryDataFragment>;
|
gallery: GQL.GalleryDataFragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GalleryViewer: React.FC<IProps> = ({ gallery }) => {
|
export const GalleryViewer: React.FC<IProps> = ({ gallery }) => {
|
||||||
const [lightboxToggle, setLightboxToggle] = useState(false);
|
const images = gallery?.images ?? [];
|
||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
const showLightbox = useLightbox({ images, showNavigation: false });
|
||||||
|
|
||||||
const openImage = (index: number) => {
|
const thumbs = images.map((file, index) => (
|
||||||
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
|
<div
|
||||||
role="link"
|
role="link"
|
||||||
tabIndex={index}
|
tabIndex={index}
|
||||||
key={file.checksum ?? index}
|
key={file.checksum ?? index}
|
||||||
onClick={() => openImage(index)}
|
onClick={() => showLightbox(index)}
|
||||||
onKeyPress={() => openImage(index)}
|
onKeyPress={() => showLightbox(index)}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={file.paths.thumbnail ?? ""}
|
src={file.paths.thumbnail ?? ""}
|
||||||
@@ -41,12 +31,6 @@ export const GalleryViewer: React.FC<IProps> = ({ gallery }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="gallery">
|
<div className="gallery">
|
||||||
<div className="flexbin">{thumbs}</div>
|
<div className="flexbin">{thumbs}</div>
|
||||||
<FsLightbox
|
|
||||||
sourceIndex={currentIndex}
|
|
||||||
toggler={lightboxToggle}
|
|
||||||
sources={photos}
|
|
||||||
key={gallery.id!}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
68
ui/v2.5/src/components/Galleries/GalleryWallCard.tsx
Normal file
68
ui/v2.5/src/components/Galleries/GalleryWallCard.tsx
Normal 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;
|
||||||
@@ -96,3 +96,116 @@ $galleryTabWidth: 450px;
|
|||||||
height: calc(1.5em + 0.75rem + 2px);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import FsLightbox from "fslightbox-react";
|
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import {
|
import {
|
||||||
FindImagesQueryResult,
|
FindImagesQueryResult,
|
||||||
@@ -9,7 +8,7 @@ import {
|
|||||||
} from "src/core/generated-graphql";
|
} from "src/core/generated-graphql";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { queryFindImages } from "src/core/StashService";
|
import { queryFindImages } from "src/core/StashService";
|
||||||
import { useImagesList } from "src/hooks";
|
import { useImagesList, useLightbox } from "src/hooks";
|
||||||
import { TextUtils } from "src/utils";
|
import { TextUtils } from "src/utils";
|
||||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { DisplayMode } from "src/models/list-filter/types";
|
import { DisplayMode } from "src/models/list-filter/types";
|
||||||
@@ -22,25 +21,45 @@ import { ExportDialog } from "../Shared/ExportDialog";
|
|||||||
|
|
||||||
interface IImageWallProps {
|
interface IImageWallProps {
|
||||||
images: GQL.SlimImageDataFragment[];
|
images: GQL.SlimImageDataFragment[];
|
||||||
|
onChangePage: (page: number) => void;
|
||||||
|
currentPage: number;
|
||||||
|
pageCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ImageWall: React.FC<IImageWallProps> = ({ images }) => {
|
const ImageWall: React.FC<IImageWallProps> = ({
|
||||||
const [lightboxToggle, setLightboxToggle] = useState(false);
|
images,
|
||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
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) => {
|
const showLightbox = useLightbox({
|
||||||
setCurrentIndex(index);
|
images,
|
||||||
setLightboxToggle(!lightboxToggle);
|
showNavigation: false,
|
||||||
};
|
pageCallback: handleLightBoxPage,
|
||||||
|
pageHeader: `Page ${currentPage} / ${pageCount}`,
|
||||||
|
});
|
||||||
|
|
||||||
const photos = images.map((image) => image.paths.image ?? "");
|
|
||||||
const thumbs = images.map((image, index) => (
|
const thumbs = images.map((image, index) => (
|
||||||
<div
|
<div
|
||||||
role="link"
|
role="link"
|
||||||
tabIndex={index}
|
tabIndex={index}
|
||||||
key={image.id}
|
key={image.id}
|
||||||
onClick={() => openImage(index)}
|
onClick={() => showLightbox(index)}
|
||||||
onKeyPress={() => openImage(index)}
|
onKeyPress={() => showLightbox(index)}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={image.paths.thumbnail ?? ""}
|
src={image.paths.thumbnail ?? ""}
|
||||||
@@ -51,32 +70,9 @@ const ImageWall: React.FC<IImageWallProps> = ({ images }) => {
|
|||||||
</div>
|
</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 (
|
return (
|
||||||
<div className="gallery">
|
<div className="gallery">
|
||||||
<div className="flexbin">{thumbs}</div>
|
<div className="flexbin">{thumbs}</div>
|
||||||
<FsLightbox
|
|
||||||
sourceIndex={currentIndex}
|
|
||||||
toggler={lightboxToggle}
|
|
||||||
sources={photos}
|
|
||||||
key={key}
|
|
||||||
onOpen={onLightboxOpen}
|
|
||||||
onClose={onLightboxClose}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -125,7 +121,7 @@ export const ImageList: React.FC<IImageList> = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const listData = useImagesList({
|
const { template, onSelectChange } = useImagesList({
|
||||||
zoomable: true,
|
zoomable: true,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
otherOperations,
|
otherOperations,
|
||||||
@@ -150,12 +146,7 @@ export const ImageList: React.FC<IImageList> = ({
|
|||||||
filterCopy.itemsPerPage = 1;
|
filterCopy.itemsPerPage = 1;
|
||||||
filterCopy.currentPage = index + 1;
|
filterCopy.currentPage = index + 1;
|
||||||
const singleResult = await queryFindImages(filterCopy);
|
const singleResult = await queryFindImages(filterCopy);
|
||||||
if (
|
if (singleResult.data.findImages.images.length === 1) {
|
||||||
singleResult &&
|
|
||||||
singleResult.data &&
|
|
||||||
singleResult.data.findImages &&
|
|
||||||
singleResult.data.findImages.images.length === 1
|
|
||||||
) {
|
|
||||||
const { id } = singleResult!.data!.findImages!.images[0];
|
const { id } = singleResult!.data!.findImages!.images[0];
|
||||||
// navigate to the image player page
|
// navigate to the image player page
|
||||||
history.push(`/images/${id}`);
|
history.push(`/images/${id}`);
|
||||||
@@ -228,7 +219,7 @@ export const ImageList: React.FC<IImageList> = ({
|
|||||||
selecting={selectedIds.size > 0}
|
selecting={selectedIds.size > 0}
|
||||||
selected={selectedIds.has(image.id)}
|
selected={selectedIds.has(image.id)}
|
||||||
onSelectedChanged={(selected: boolean, shiftKey: boolean) =>
|
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,
|
result: FindImagesQueryResult,
|
||||||
filter: ListFilterModel,
|
filter: ListFilterModel,
|
||||||
selectedIds: Set<string>,
|
selectedIds: Set<string>,
|
||||||
zoomIndex: number
|
zoomIndex: number,
|
||||||
|
onChangePage: (page: number) => void,
|
||||||
|
pageCount: number
|
||||||
) {
|
) {
|
||||||
if (!result.data || !result.data.findImages) {
|
if (!result.data || !result.data.findImages) {
|
||||||
return;
|
return;
|
||||||
@@ -252,11 +245,15 @@ export const ImageList: React.FC<IImageList> = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// if (filter.displayMode === DisplayMode.List) {
|
|
||||||
// return <ImageListTable images={result.data.findImages.images} />;
|
|
||||||
// }
|
|
||||||
if (filter.displayMode === DisplayMode.Wall) {
|
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,
|
result: FindImagesQueryResult,
|
||||||
filter: ListFilterModel,
|
filter: ListFilterModel,
|
||||||
selectedIds: Set<string>,
|
selectedIds: Set<string>,
|
||||||
zoomIndex: number
|
zoomIndex: number,
|
||||||
|
onChangePage: (page: number) => void,
|
||||||
|
pageCount: number
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{maybeRenderImageExportDialog(selectedIds)}
|
{maybeRenderImageExportDialog(selectedIds)}
|
||||||
{renderImages(result, filter, selectedIds, zoomIndex)}
|
{renderImages(
|
||||||
|
result,
|
||||||
|
filter,
|
||||||
|
selectedIds,
|
||||||
|
zoomIndex,
|
||||||
|
onChangePage,
|
||||||
|
pageCount
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return listData.template;
|
return template;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,9 +16,8 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
LoadingIndicator,
|
LoadingIndicator,
|
||||||
} from "src/components/Shared";
|
} from "src/components/Shared";
|
||||||
import { useToast } from "src/hooks";
|
import { useLightbox, useToast } from "src/hooks";
|
||||||
import { TextUtils } from "src/utils";
|
import { TextUtils } from "src/utils";
|
||||||
import FsLightbox from "fslightbox-react";
|
|
||||||
import { PerformerDetailsPanel } from "./PerformerDetailsPanel";
|
import { PerformerDetailsPanel } from "./PerformerDetailsPanel";
|
||||||
import { PerformerOperationsPanel } from "./PerformerOperationsPanel";
|
import { PerformerOperationsPanel } from "./PerformerOperationsPanel";
|
||||||
import { PerformerScenesPanel } from "./PerformerScenesPanel";
|
import { PerformerScenesPanel } from "./PerformerScenesPanel";
|
||||||
@@ -39,7 +38,6 @@ export const Performer: React.FC = () => {
|
|||||||
// Performer state
|
// Performer state
|
||||||
const [imagePreview, setImagePreview] = useState<string | null>();
|
const [imagePreview, setImagePreview] = useState<string | null>();
|
||||||
const [imageEncoding, setImageEncoding] = useState<boolean>(false);
|
const [imageEncoding, setImageEncoding] = useState<boolean>(false);
|
||||||
const [lightboxToggle, setLightboxToggle] = useState(false);
|
|
||||||
const { data, loading: performerLoading, error } = useFindPerformer(id);
|
const { data, loading: performerLoading, error } = useFindPerformer(id);
|
||||||
const performer = data?.findPerformer || ({} as Partial<GQL.Performer>);
|
const performer = data?.findPerformer || ({} as Partial<GQL.Performer>);
|
||||||
|
|
||||||
@@ -51,6 +49,10 @@ export const Performer: React.FC = () => {
|
|||||||
? performer.image_path ?? ""
|
? performer.image_path ?? ""
|
||||||
: imagePreview ?? `${performer.image_path}?default=true`;
|
: imagePreview ?? `${performer.image_path}?default=true`;
|
||||||
|
|
||||||
|
const showLightbox = useLightbox({
|
||||||
|
images: [{ paths: { thumbnail: activeImage, image: activeImage } }],
|
||||||
|
});
|
||||||
|
|
||||||
// Network state
|
// Network state
|
||||||
const [loading, setIsLoading] = useState(false);
|
const [loading, setIsLoading] = useState(false);
|
||||||
const isLoading = performerLoading || loading;
|
const isLoading = performerLoading || loading;
|
||||||
@@ -318,10 +320,7 @@ export const Performer: React.FC = () => {
|
|||||||
{imageEncoding ? (
|
{imageEncoding ? (
|
||||||
<LoadingIndicator message="Encoding image..." />
|
<LoadingIndicator message="Encoding image..." />
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button variant="link" onClick={() => showLightbox()}>
|
||||||
variant="link"
|
|
||||||
onClick={() => setLightboxToggle(!lightboxToggle)}
|
|
||||||
>
|
|
||||||
<img className="performer" src={activeImage} alt="Performer" />
|
<img className="performer" src={activeImage} alt="Performer" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -342,7 +341,6 @@ export const Performer: React.FC = () => {
|
|||||||
<div className="performer-tabs">{renderTabs()}</div>
|
<div className="performer-tabs">{renderTabs()}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<FsLightbox toggler={lightboxToggle} sources={[activeImage]} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { ErrorMessage, LoadingIndicator, Icon } from "src/components/Shared";
|
|||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { ScenePlayer } from "src/components/ScenePlayer";
|
import { ScenePlayer } from "src/components/ScenePlayer";
|
||||||
import { TextUtils, JWUtils } from "src/utils";
|
import { TextUtils, JWUtils } from "src/utils";
|
||||||
import * as Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import { SceneMarkersPanel } from "./SceneMarkersPanel";
|
import { SceneMarkersPanel } from "./SceneMarkersPanel";
|
||||||
import { SceneFileInfoPanel } from "./SceneFileInfoPanel";
|
import { SceneFileInfoPanel } from "./SceneFileInfoPanel";
|
||||||
import { SceneEditPanel } from "./SceneEditPanel";
|
import { SceneEditPanel } from "./SceneEditPanel";
|
||||||
|
|||||||
35
ui/v2.5/src/components/Shared/RatingStars.tsx
Normal file
35
ui/v2.5/src/components/Shared/RatingStars.tsx
Normal 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>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
);
|
||||||
@@ -23,3 +23,4 @@ export { default as SuccessIcon } from "./SuccessIcon";
|
|||||||
export { default as ErrorMessage } from "./ErrorMessage";
|
export { default as ErrorMessage } from "./ErrorMessage";
|
||||||
export { default as TruncatedText } from "./TruncatedText";
|
export { default as TruncatedText } from "./TruncatedText";
|
||||||
export { BasicCard } from "./BasicCard";
|
export { BasicCard } from "./BasicCard";
|
||||||
|
export { RatingStars } from "./RatingStars";
|
||||||
|
|||||||
@@ -182,3 +182,17 @@ button.collapse-button.btn-primary:not(:disabled):not(.disabled):active {
|
|||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.RatingStars {
|
||||||
|
&-unfilled {
|
||||||
|
path {
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-filled {
|
||||||
|
path {
|
||||||
|
fill: gold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
350
ui/v2.5/src/hooks/Lightbox/Lightbox.tsx
Normal file
350
ui/v2.5/src/hooks/Lightbox/Lightbox.tsx
Normal 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;
|
||||||
|
};
|
||||||
54
ui/v2.5/src/hooks/Lightbox/context.tsx
Normal file
54
ui/v2.5/src/hooks/Lightbox/context.tsx
Normal 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;
|
||||||
73
ui/v2.5/src/hooks/Lightbox/hooks.ts
Normal file
73
ui/v2.5/src/hooks/Lightbox/hooks.ts
Normal 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;
|
||||||
|
};
|
||||||
2
ui/v2.5/src/hooks/Lightbox/index.ts
Normal file
2
ui/v2.5/src/hooks/Lightbox/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./context";
|
||||||
|
export * from "./hooks";
|
||||||
124
ui/v2.5/src/hooks/Lightbox/lightbox.scss
Normal file
124
ui/v2.5/src/hooks/Lightbox/lightbox.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -64,6 +64,7 @@ interface IListHookData {
|
|||||||
filter: ListFilterModel;
|
filter: ListFilterModel;
|
||||||
template: React.ReactElement;
|
template: React.ReactElement;
|
||||||
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void;
|
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void;
|
||||||
|
onChangePage: (page: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IListHookOperation<T> {
|
export interface IListHookOperation<T> {
|
||||||
@@ -92,7 +93,9 @@ interface IListHookOptions<T, E> {
|
|||||||
result: T,
|
result: T,
|
||||||
filter: ListFilterModel,
|
filter: ListFilterModel,
|
||||||
selectedIds: Set<string>,
|
selectedIds: Set<string>,
|
||||||
zoomIndex: number
|
zoomIndex: number,
|
||||||
|
onChangePage: (page: number) => void,
|
||||||
|
pageCount: number
|
||||||
) => React.ReactNode;
|
) => React.ReactNode;
|
||||||
renderEditDialog?: (
|
renderEditDialog?: (
|
||||||
selected: E[],
|
selected: E[],
|
||||||
@@ -350,10 +353,19 @@ const RenderList = <
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pages = Math.ceil(totalCount / filter.itemsPerPage);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{renderPagination()}
|
{renderPagination()}
|
||||||
{renderContent(result, filter, selectedIds, zoomIndex)}
|
{renderContent(
|
||||||
|
result,
|
||||||
|
filter,
|
||||||
|
selectedIds,
|
||||||
|
zoomIndex,
|
||||||
|
onChangePage,
|
||||||
|
pages
|
||||||
|
)}
|
||||||
<PaginationIndex
|
<PaginationIndex
|
||||||
itemsPerPage={filter.itemsPerPage}
|
itemsPerPage={filter.itemsPerPage}
|
||||||
currentPage={filter.currentPage}
|
currentPage={filter.currentPage}
|
||||||
@@ -525,6 +537,7 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
|||||||
filter,
|
filter,
|
||||||
template,
|
template,
|
||||||
onSelectChange,
|
onSelectChange,
|
||||||
|
onChangePage,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,3 +8,4 @@ export {
|
|||||||
useStudiosList,
|
useStudiosList,
|
||||||
usePerformersList,
|
usePerformersList,
|
||||||
} from "./ListHook";
|
} from "./ListHook";
|
||||||
|
export { useLightbox, useGalleryLightbox } from "./Lightbox";
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
@import "src/components/Wall/styles.scss";
|
@import "src/components/Wall/styles.scss";
|
||||||
@import "../node_modules/flag-icon-css/css/flag-icon.min.css";
|
@import "../node_modules/flag-icon-css/css/flag-icon.min.css";
|
||||||
@import "src/components/Tagger/styles.scss";
|
@import "src/components/Tagger/styles.scss";
|
||||||
|
@import "src/hooks/Lightbox/lightbox.scss";
|
||||||
|
|
||||||
/* stylelint-disable */
|
/* stylelint-disable */
|
||||||
#root {
|
#root {
|
||||||
|
|||||||
@@ -248,6 +248,11 @@ export class ListFilterModel {
|
|||||||
new PerformersCriterionOption(),
|
new PerformersCriterionOption(),
|
||||||
new StudiosCriterionOption(),
|
new StudiosCriterionOption(),
|
||||||
];
|
];
|
||||||
|
this.displayModeOptions = [
|
||||||
|
DisplayMode.Grid,
|
||||||
|
DisplayMode.List,
|
||||||
|
DisplayMode.Wall,
|
||||||
|
];
|
||||||
break;
|
break;
|
||||||
case FilterMode.SceneMarkers:
|
case FilterMode.SceneMarkers:
|
||||||
this.sortBy = "title";
|
this.sortBy = "title";
|
||||||
|
|||||||
@@ -3281,14 +3281,6 @@ anymatch@^3.0.3, anymatch@~3.1.1:
|
|||||||
normalize-path "^3.0.0"
|
normalize-path "^3.0.0"
|
||||||
picomatch "^2.0.4"
|
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:
|
apollo-upload-client@^14.1.2:
|
||||||
version "14.1.2"
|
version "14.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-upload-client/-/apollo-upload-client-14.1.2.tgz#7a72b000f1cd67eaf8f12b4bda2796d0898c0dae"
|
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"
|
resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
|
||||||
integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==
|
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"
|
version "2.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
|
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
|
||||||
integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
|
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"
|
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.5.3.tgz#c6a72b355aaf323920be800246a6e4ef30997fe6"
|
||||||
integrity sha512-o9ppKQioXGqhw8Z7mah6KdTYpNQY//tipnkxppWhPbiSWdD+1raYsnhwEZjkTHYbGee4cVQ0Rx65EhOY/HNLcQ==
|
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:
|
brace-expansion@^1.1.7:
|
||||||
version "1.1.11"
|
version "1.1.11"
|
||||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
||||||
@@ -5544,13 +5531,6 @@ dom-converter@^0.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
utila "~0.4"
|
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:
|
dom-helpers@^5.0.1, dom-helpers@^5.1.2, dom-helpers@^5.2.0:
|
||||||
version "5.2.0"
|
version "5.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.0.tgz#57fd054c5f8f34c52a3eeffdb7e7e93cd357d95b"
|
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.0.tgz#57fd054c5f8f34c52a3eeffdb7e7e93cd357d95b"
|
||||||
@@ -6285,11 +6265,6 @@ execall@^2.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
clone-regexp "^2.1.0"
|
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:
|
exif-parser@^0.1.12:
|
||||||
version "0.1.12"
|
version "0.1.12"
|
||||||
resolved "https://registry.yarnpkg.com/exif-parser/-/exif-parser-0.1.12.tgz#58a9d2d72c02c1f6f02a0ef4a9166272b7760922"
|
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"
|
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e"
|
||||||
integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==
|
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:
|
fstream@^1.0.0, fstream@^1.0.12:
|
||||||
version "1.0.12"
|
version "1.0.12"
|
||||||
resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045"
|
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"
|
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
|
||||||
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
|
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:
|
i18n-iso-countries@^6.0.0:
|
||||||
version "6.2.2"
|
version "6.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/i18n-iso-countries/-/i18n-iso-countries-6.2.2.tgz#6b63d00e90ee4022e8c159a9e688d2a8156b0e0b"
|
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"
|
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
|
||||||
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
|
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:
|
inquirer@7.3.3, inquirer@^7.3.3:
|
||||||
version "7.3.3"
|
version "7.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003"
|
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"
|
react-is "^16.3.2"
|
||||||
warning "^4.0.0"
|
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"
|
version "15.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
|
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
|
||||||
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
|
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"
|
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
|
||||||
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
|
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:
|
react-input-autosize@^2.2.2:
|
||||||
version "2.2.2"
|
version "2.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.2.tgz#fcaa7020568ec206bc04be36f4eb68e647c4d8c2"
|
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"
|
uncontrollable "^7.0.0"
|
||||||
warning "^4.0.3"
|
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:
|
react-refresh@^0.8.3:
|
||||||
version "0.8.3"
|
version "0.8.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
|
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
|
||||||
@@ -12389,14 +12323,6 @@ react-scripts@^4.0.0:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "^2.1.3"
|
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:
|
react-select@^3.1.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-select/-/react-select-3.1.0.tgz#ab098720b2e9fe275047c993f0d0caf5ded17c27"
|
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-input-autosize "^2.2.2"
|
||||||
react-transition-group "^4.3.0"
|
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:
|
react-transition-group@^4.3.0, react-transition-group@^4.4.1:
|
||||||
version "4.4.1"
|
version "4.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9"
|
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"
|
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
|
||||||
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
|
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:
|
resolve-cwd@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
|
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
|
||||||
|
|||||||
Reference in New Issue
Block a user