diff --git a/ui/v2.5/src/components/Shared/ImageSelector.tsx b/ui/v2.5/src/components/Shared/ImageSelector.tsx new file mode 100644 index 000000000..03ae4c9cc --- /dev/null +++ b/ui/v2.5/src/components/Shared/ImageSelector.tsx @@ -0,0 +1,102 @@ +import React, { useEffect, useState } from "react"; +import cx from "classnames"; +import { LoadingIndicator } from "./LoadingIndicator"; +import { Button } from "react-bootstrap"; +import { faArrowLeft, faArrowRight } from "@fortawesome/free-solid-svg-icons"; +import { Icon } from "./Icon"; +import { FormattedMessage } from "react-intl"; + +interface IImageSelectorProps { + imageClassName?: string; + images: string[]; + imageIndex: number; + setImageIndex: (index: number) => void; +} + +export const ImageSelector: React.FC = ({ + imageClassName, + images, + imageIndex, + setImageIndex, +}) => { + const [imageState, setImageState] = useState< + "loading" | "error" | "loaded" | "empty" + >("empty"); + const [loadDict, setLoadDict] = useState>({}); + const [currentImage, setCurrentImage] = useState(""); + + useEffect(() => { + if (imageState !== "loading") { + setCurrentImage(images[imageIndex]); + } + }, [imageState, imageIndex, images]); + + const changeImage = (index: number) => { + setImageIndex(index); + if (!loadDict[index]) setImageState("loading"); + }; + + const setPrev = () => + changeImage(imageIndex === 0 ? images.length - 1 : imageIndex - 1); + const setNext = () => + changeImage(imageIndex === images.length - 1 ? 0 : imageIndex + 1); + + const handleLoad = (index: number) => { + setLoadDict({ + ...loadDict, + [index]: true, + }); + setImageState("loaded"); + }; + const handleError = () => setImageState("error"); + + return ( +
+ {images.length > 1 && ( +
+ +
+ +
+ +
+ )} + +
+ {/* hidden image to handle loading */} + handleLoad(imageIndex)} + onError={handleError} + /> + + {imageState === "loading" && } + {imageState === "error" && ( +
+ + + +
+ )} +
+
+ ); +}; diff --git a/ui/v2.5/src/components/Shared/LoadingIndicator.tsx b/ui/v2.5/src/components/Shared/LoadingIndicator.tsx index 3b9247c91..661722d73 100644 --- a/ui/v2.5/src/components/Shared/LoadingIndicator.tsx +++ b/ui/v2.5/src/components/Shared/LoadingIndicator.tsx @@ -1,6 +1,7 @@ import React from "react"; import { Spinner } from "react-bootstrap"; import cx from "classnames"; +import { useIntl } from "react-intl"; interface ILoadingProps { message?: string; @@ -17,13 +18,19 @@ export const LoadingIndicator: React.FC = ({ inline = false, small = false, card = false, -}) => ( -
- - Loading... - - {message !== "" && ( -

{message ?? "Loading..."}

- )} -
-); +}) => { + const intl = useIntl(); + + const text = intl.formatMessage({ id: "loading.generic" }); + + return ( +
+ + {text} + + {message !== "" && ( +

{message ?? text}

+ )} +
+ ); +}; diff --git a/ui/v2.5/src/components/Shared/ScrapeDialog.tsx b/ui/v2.5/src/components/Shared/ScrapeDialog.tsx index ca32c72cd..425419ab0 100644 --- a/ui/v2.5/src/components/Shared/ScrapeDialog.tsx +++ b/ui/v2.5/src/components/Shared/ScrapeDialog.tsx @@ -15,8 +15,6 @@ import isEqual from "lodash-es/isEqual"; import clone from "lodash-es/clone"; import { FormattedMessage, useIntl } from "react-intl"; import { - faArrowLeft, - faArrowRight, faCheck, faPencilAlt, faPlus, @@ -25,6 +23,7 @@ import { import { getCountryByISO } from "src/utils/country"; import { CountrySelect } from "./CountrySelect"; import { StringListInput } from "./StringListInput"; +import { ImageSelector } from "./ImageSelector"; export class ScrapeResult { public newValue?: T; @@ -443,135 +442,6 @@ export const ScrapedImageRow: React.FC = (props) => { ); }; -interface IScrapedImageDialogRowProps< - T extends ScrapeResult, - V extends IHasName -> extends IScrapedFieldProps { - title: string; - renderOriginalField: () => JSX.Element | undefined; - renderNewField: () => JSX.Element | undefined; - onChange: (value: T) => void; - newValues?: V[]; - images: string[]; - onCreateNew?: (index: number) => void; -} - -export const ScrapeImageDialogRow = < - T extends ScrapeResult, - V extends IHasName ->( - props: IScrapedImageDialogRowProps -) => { - const [imageIndex, setImageIndex] = useState(0); - - function hasNewValues() { - return props.newValues && props.newValues.length > 0 && props.onCreateNew; - } - - function setPrev() { - let newIdx = imageIndex - 1; - if (newIdx < 0) { - newIdx = props.images.length - 1; - } - const ret = props.result.cloneWithValue(props.images[newIdx]); - props.onChange(ret as T); - setImageIndex(newIdx); - } - - function setNext() { - let newIdx = imageIndex + 1; - if (newIdx >= props.images.length) { - newIdx = 0; - } - const ret = props.result.cloneWithValue(props.images[newIdx]); - props.onChange(ret as T); - setImageIndex(newIdx); - } - - if (!props.result.scraped && !hasNewValues()) { - return <>; - } - - function renderSelector() { - return ( - props.images.length > 1 && ( -
- -
- Select performer image -
- {imageIndex + 1} of {props.images.length} -
- -
- ) - ); - } - - function renderNewValues() { - if (!hasNewValues()) { - return; - } - - const ret = ( - <> - {props.newValues!.map((t, i) => ( - props.onCreateNew!(i)} - > - {t.name} - - - ))} - - ); - - const minCollapseLength = 10; - - if (props.newValues!.length >= minCollapseLength) { - return ( - - {ret} - - ); - } - - return ret; - } - - return ( - - - {props.title} - - - - - - {props.renderOriginalField()} - - - - {props.renderNewField()} - {renderSelector()} - - {renderNewValues()} - - - - - ); -}; - interface IScrapedImagesRowProps { title: string; className?: string; @@ -581,11 +451,18 @@ interface IScrapedImagesRowProps { } export const ScrapedImagesRow: React.FC = (props) => { + const [imageIndex, setImageIndex] = useState(0); + + function onSetImageIndex(newIdx: number) { + const ret = props.result.cloneWithValue(props.images[newIdx]); + props.onChange(ret); + setImageIndex(newIdx); + } + return ( - ( = (props) => { /> )} renderNewField={() => ( - +
+ +
)} onChange={props.onChange} /> diff --git a/ui/v2.5/src/components/Shared/styles.scss b/ui/v2.5/src/components/Shared/styles.scss index 0a31694fc..97bf7817f 100644 --- a/ui/v2.5/src/components/Shared/styles.scss +++ b/ui/v2.5/src/components/Shared/styles.scss @@ -135,10 +135,36 @@ } } -.scrape-dialog .modal-content .dialog-container { - max-height: calc(100vh - 14rem); - overflow-y: auto; - padding-right: 15px; +.scrape-dialog { + .modal-content .dialog-container { + max-height: calc(100vh - 14rem); + overflow-y: auto; + padding-right: 15px; + } + + .image-selection { + .select-buttons { + align-items: center; + display: flex; + justify-content: space-between; + margin-top: 1rem; + + .image-index { + flex-grow: 1; + text-align: center; + } + } + + .loading { + opacity: 0.5; + } + + .LoadingIndicator { + height: 100%; + position: absolute; + top: 0; + } + } } button.collapse-button.btn-primary:not(:disabled):not(.disabled):hover, diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index 8e5263ea9..6775e71ca 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -949,6 +949,7 @@ }, "empty_server": "Add some scenes to your server to view recommendations on this page.", "errors": { + "loading_type": "Error loading {type}", "image_index_greater_than_zero": "Image index must be greater than 0", "lazy_component_error_help": "If you recently upgraded Stash, please reload the page or clear your browser cache.", "something_went_wrong": "Something went wrong." @@ -1013,6 +1014,7 @@ "include_parent_tags": "Include parent tags", "include_sub_studios": "Include subsidiary studios", "include_sub_tags": "Include sub-tags", + "index_of_total": "{index} of {total}", "instagram": "Instagram", "interactive": "Interactive", "interactive_speed": "Interactive speed",