From 6453e6c18d2cf13cb1d8224a8b93ff8405b0c08b Mon Sep 17 00:00:00 2001 From: CJ <72030708+cj12312021@users.noreply.github.com> Date: Mon, 5 Feb 2024 20:06:47 -0600 Subject: [PATCH] Fit cards properly within their containers (#4514) * created missing cards grids --- ui/v2.5/src/components/FrontPage/styles.scss | 4 -- .../src/components/Galleries/GalleryCard.tsx | 38 +++++++++++++- .../src/components/Galleries/GalleryList.tsx | 9 +++- ui/v2.5/src/components/Images/ImageCard.tsx | 39 ++++++++++++++- ui/v2.5/src/components/Images/ImageList.tsx | 8 ++- ui/v2.5/src/components/Movies/MovieCard.tsx | 20 +++++++- .../src/components/Movies/MovieCardGrid.tsx | 35 +++++++++++++ ui/v2.5/src/components/Movies/MovieList.tsx | 20 +++----- .../components/Performers/PerformerCard.tsx | 20 +++++++- .../Performers/PerformerCardGrid.tsx | 39 +++++++++++++++ .../components/Performers/PerformerList.tsx | 24 ++++----- ui/v2.5/src/components/Performers/styles.scss | 2 +- .../components/ScenePlayer/ScenePlayer.tsx | 3 +- ui/v2.5/src/components/Scenes/SceneCard.tsx | 39 ++++++++++++++- .../src/components/Scenes/SceneCardsGrid.tsx | 8 ++- ui/v2.5/src/components/Scenes/styles.scss | 10 ++++ ui/v2.5/src/components/Shared/GridCard.tsx | 49 ++++++++++++++++++- ui/v2.5/src/components/Studios/StudioCard.tsx | 21 +++++++- .../src/components/Studios/StudioCardGrid.tsx | 38 ++++++++++++++ ui/v2.5/src/components/Studios/StudioList.tsx | 22 +++------ ui/v2.5/src/components/Tags/TagCard.tsx | 36 +++++++++++++- ui/v2.5/src/components/Tags/TagCardGrid.tsx | 38 ++++++++++++++ ui/v2.5/src/components/Tags/TagList.tsx | 22 +++------ ui/v2.5/src/index.scss | 38 -------------- ui/v2.5/src/utils/screen.ts | 2 +- 25 files changed, 460 insertions(+), 124 deletions(-) create mode 100644 ui/v2.5/src/components/Movies/MovieCardGrid.tsx create mode 100644 ui/v2.5/src/components/Performers/PerformerCardGrid.tsx create mode 100644 ui/v2.5/src/components/Studios/StudioCardGrid.tsx create mode 100644 ui/v2.5/src/components/Tags/TagCardGrid.tsx diff --git a/ui/v2.5/src/components/FrontPage/styles.scss b/ui/v2.5/src/components/FrontPage/styles.scss index e4049b5aa58..cb03087dde4 100644 --- a/ui/v2.5/src/components/FrontPage/styles.scss +++ b/ui/v2.5/src/components/FrontPage/styles.scss @@ -319,10 +319,6 @@ .slick-list .performer-card.card { width: 16rem; } - - .slick-list .performer-card-image { - height: 24rem; - } } /* Icons */ diff --git a/ui/v2.5/src/components/Galleries/GalleryCard.tsx b/ui/v2.5/src/components/Galleries/GalleryCard.tsx index 8dd49534195..4df51efab5f 100644 --- a/ui/v2.5/src/components/Galleries/GalleryCard.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryCard.tsx @@ -1,8 +1,8 @@ import { Button, ButtonGroup, OverlayTrigger, Tooltip } from "react-bootstrap"; -import React from "react"; +import React, { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import * as GQL from "src/core/generated-graphql"; -import { GridCard } from "../Shared/GridCard"; +import { GridCard, calculateCardWidth } from "../Shared/GridCard"; import { HoverPopover } from "../Shared/HoverPopover"; import { Icon } from "../Shared/Icon"; import { SceneLink, TagLink } from "../Shared/TagLink"; @@ -14,9 +14,11 @@ import { ConfigurationContext } from "src/hooks/Config"; import { RatingBanner } from "../Shared/RatingBanner"; import { faBox, faPlayCircle, faTag } from "@fortawesome/free-solid-svg-icons"; import { galleryTitle } from "src/core/galleries"; +import ScreenUtils from "src/utils/screen"; interface IProps { gallery: GQL.SlimGalleryDataFragment; + containerWidth?: number; selecting?: boolean; selected?: boolean | undefined; zoomIndex?: number; @@ -26,6 +28,37 @@ interface IProps { export const GalleryCard: React.FC = (props) => { const { configuration } = React.useContext(ConfigurationContext); const showStudioAsText = configuration?.interface.showStudioAsText ?? false; + const [cardWidth, setCardWidth] = useState(); + + useEffect(() => { + if ( + !props.containerWidth || + props.zoomIndex === undefined || + ScreenUtils.isMobile() + ) + return; + + let zoomValue = props.zoomIndex; + let preferredCardWidth: number; + switch (zoomValue) { + case 0: + preferredCardWidth = 240; + break; + case 1: + preferredCardWidth = 340; + break; + case 2: + preferredCardWidth = 480; + break; + case 3: + preferredCardWidth = 640; + } + let fittedCardWidth = calculateCardWidth( + props.containerWidth, + preferredCardWidth! + ); + setCardWidth(fittedCardWidth); + }, [props, props.containerWidth, props.zoomIndex]); function maybeRenderScenePopoverButton() { if (props.gallery.scenes.length === 0) return; @@ -153,6 +186,7 @@ export const GalleryCard: React.FC = (props) => { = ({ setIsExportDialogOpen(true); } + const componentRef = useRef(null); + const { width } = useContainerDimensions(componentRef); + function renderContent( result: GQL.FindGalleriesQueryResult, filter: ListFilterModel, @@ -133,10 +137,11 @@ export const GalleryList: React.FC = ({ if (filter.displayMode === DisplayMode.Grid) { return ( -
+
{result.data.findGalleries.galleries.map((gallery) => ( 0} diff --git a/ui/v2.5/src/components/Images/ImageCard.tsx b/ui/v2.5/src/components/Images/ImageCard.tsx index 47f7eac7a79..98ae5b2fb86 100644 --- a/ui/v2.5/src/components/Images/ImageCard.tsx +++ b/ui/v2.5/src/components/Images/ImageCard.tsx @@ -1,4 +1,4 @@ -import React, { MouseEvent, useMemo } from "react"; +import React, { MouseEvent, useEffect, useMemo, useState } from "react"; import { Button, ButtonGroup } from "react-bootstrap"; import cx from "classnames"; import * as GQL from "src/core/generated-graphql"; @@ -7,7 +7,7 @@ import { GalleryLink, TagLink } from "src/components/Shared/TagLink"; import { HoverPopover } from "src/components/Shared/HoverPopover"; import { SweatDrops } from "src/components/Shared/SweatDrops"; import { PerformerPopoverButton } from "src/components/Shared/PerformerPopoverButton"; -import { GridCard } from "src/components/Shared/GridCard"; +import { GridCard, calculateCardWidth } from "src/components/Shared/GridCard"; import { RatingBanner } from "src/components/Shared/RatingBanner"; import { faBox, @@ -17,9 +17,11 @@ import { } from "@fortawesome/free-solid-svg-icons"; import { objectTitle } from "src/core/files"; import { TruncatedText } from "../Shared/TruncatedText"; +import ScreenUtils from "src/utils/screen"; interface IImageCardProps { image: GQL.SlimImageDataFragment; + containerWidth?: number; selecting?: boolean; selected?: boolean | undefined; zoomIndex: number; @@ -30,6 +32,38 @@ interface IImageCardProps { export const ImageCard: React.FC = ( props: IImageCardProps ) => { + const [cardWidth, setCardWidth] = useState(); + + useEffect(() => { + if ( + !props.containerWidth || + props.zoomIndex === undefined || + ScreenUtils.isMobile() + ) + return; + + let zoomValue = props.zoomIndex; + let preferredCardWidth: number; + switch (zoomValue) { + case 0: + preferredCardWidth = 240; + break; + case 1: + preferredCardWidth = 340; + break; + case 2: + preferredCardWidth = 480; + break; + case 3: + preferredCardWidth = 640; + } + let fittedCardWidth = calculateCardWidth( + props.containerWidth, + preferredCardWidth! + ); + setCardWidth(fittedCardWidth); + }, [props, props.containerWidth, props.zoomIndex]); + const file = useMemo( () => props.image.visual_files.length > 0 @@ -153,6 +187,7 @@ export const ImageCard: React.FC = ( = ({ ev.preventDefault(); } + const componentRef = useRef(null); + const { width } = useContainerDimensions(componentRef); + function renderImageCard( index: number, image: GQL.SlimImageDataFragment, @@ -204,6 +209,7 @@ const ImageListImages: React.FC = ({ return ( 0} @@ -220,7 +226,7 @@ const ImageListImages: React.FC = ({ if (filter.displayMode === DisplayMode.Grid) { return ( -
+
{images.map((image, index) => renderImageCard(index, image, filter.zoomIndex) )} diff --git a/ui/v2.5/src/components/Movies/MovieCard.tsx b/ui/v2.5/src/components/Movies/MovieCard.tsx index e86856f2ec8..35a87333d1e 100644 --- a/ui/v2.5/src/components/Movies/MovieCard.tsx +++ b/ui/v2.5/src/components/Movies/MovieCard.tsx @@ -1,7 +1,7 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import { Button, ButtonGroup } from "react-bootstrap"; import * as GQL from "src/core/generated-graphql"; -import { GridCard } from "../Shared/GridCard"; +import { GridCard, calculateCardWidth } from "../Shared/GridCard"; import { HoverPopover } from "../Shared/HoverPopover"; import { Icon } from "../Shared/Icon"; import { SceneLink } from "../Shared/TagLink"; @@ -9,9 +9,11 @@ import { TruncatedText } from "../Shared/TruncatedText"; import { FormattedMessage } from "react-intl"; import { RatingBanner } from "../Shared/RatingBanner"; import { faPlayCircle } from "@fortawesome/free-solid-svg-icons"; +import ScreenUtils from "src/utils/screen"; interface IProps { movie: GQL.MovieDataFragment; + containerWidth?: number; sceneIndex?: number; selecting?: boolean; selected?: boolean; @@ -19,6 +21,19 @@ interface IProps { } export const MovieCard: React.FC = (props: IProps) => { + const [cardWidth, setCardWidth] = useState(); + + useEffect(() => { + if (!props.containerWidth || ScreenUtils.isMobile()) return; + + let preferredCardWidth = 250; + let fittedCardWidth = calculateCardWidth( + props.containerWidth, + preferredCardWidth! + ); + setCardWidth(fittedCardWidth); + }, [props, props.containerWidth]); + function maybeRenderSceneNumber() { if (!props.sceneIndex) return; @@ -71,6 +86,7 @@ export const MovieCard: React.FC = (props: IProps) => { ; + onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void; +} + +export const MovieCardGrid: React.FC = ({ + movies, + selectedIds, + onSelectChange, +}) => { + const componentRef = useRef(null); + const { width } = useContainerDimensions(componentRef); + return ( +
+ {movies.map((p) => ( + 0} + selected={selectedIds.has(p.id)} + onSelectedChanged={(selected: boolean, shiftKey: boolean) => + onSelectChange(p.id, selected, shiftKey) + } + /> + ))} +
+ ); +}; diff --git a/ui/v2.5/src/components/Movies/MovieList.tsx b/ui/v2.5/src/components/Movies/MovieList.tsx index c5b6c8dedf6..55b34a783f2 100644 --- a/ui/v2.5/src/components/Movies/MovieList.tsx +++ b/ui/v2.5/src/components/Movies/MovieList.tsx @@ -18,7 +18,7 @@ import { } from "../List/ItemList"; import { ExportDialog } from "../Shared/ExportDialog"; import { DeleteEntityDialog } from "../Shared/DeleteEntityDialog"; -import { MovieCard } from "./MovieCard"; +import { MovieCardGrid } from "./MovieCardGrid"; import { EditMoviesDialog } from "./EditMoviesDialog"; const MovieItemList = makeItemList({ @@ -130,19 +130,11 @@ export const MovieList: React.FC = ({ filterHook, alterQuery }) => { if (filter.displayMode === DisplayMode.Grid) { return ( -
- {result.data.findMovies.movies.map((p) => ( - 0} - selected={selectedIds.has(p.id)} - onSelectedChanged={(selected: boolean, shiftKey: boolean) => - onSelectChange(p.id, selected, shiftKey) - } - /> - ))} -
+ ); } if (filter.displayMode === DisplayMode.List) { diff --git a/ui/v2.5/src/components/Performers/PerformerCard.tsx b/ui/v2.5/src/components/Performers/PerformerCard.tsx index a143dd57e7c..e4a83f42498 100644 --- a/ui/v2.5/src/components/Performers/PerformerCard.tsx +++ b/ui/v2.5/src/components/Performers/PerformerCard.tsx @@ -1,10 +1,10 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import { useIntl } from "react-intl"; import * as GQL from "src/core/generated-graphql"; import NavUtils from "src/utils/navigation"; import TextUtils from "src/utils/text"; -import { GridCard } from "../Shared/GridCard"; +import { GridCard, calculateCardWidth } from "../Shared/GridCard"; import { CountryFlag } from "../Shared/CountryFlag"; import { SweatDrops } from "../Shared/SweatDrops"; import { HoverPopover } from "../Shared/HoverPopover"; @@ -22,6 +22,7 @@ import { RatingBanner } from "../Shared/RatingBanner"; import cx from "classnames"; import { usePerformerUpdate } from "src/core/StashService"; import { ILabeledId } from "src/models/list-filter/types"; +import ScreenUtils from "src/utils/screen"; export interface IPerformerCardExtraCriteria { scenes?: Criterion[]; @@ -33,6 +34,7 @@ export interface IPerformerCardExtraCriteria { interface IPerformerCardProps { performer: GQL.PerformerDataFragment; + containerWidth?: number; ageFromDate?: string; selecting?: boolean; selected?: boolean; @@ -42,6 +44,7 @@ interface IPerformerCardProps { export const PerformerCard: React.FC = ({ performer, + containerWidth, ageFromDate, selecting, selected, @@ -66,6 +69,18 @@ export const PerformerCard: React.FC = ({ ); const [updatePerformer] = usePerformerUpdate(); + const [cardWidth, setCardWidth] = useState(); + + useEffect(() => { + if (!containerWidth || ScreenUtils.isMobile()) return; + + let preferredCardWidth = 300; + let fittedCardWidth = calculateCardWidth( + containerWidth, + preferredCardWidth! + ); + setCardWidth(fittedCardWidth); + }, [containerWidth]); function renderFavoriteIcon() { return ( @@ -251,6 +266,7 @@ export const PerformerCard: React.FC = ({ } diff --git a/ui/v2.5/src/components/Performers/PerformerCardGrid.tsx b/ui/v2.5/src/components/Performers/PerformerCardGrid.tsx new file mode 100644 index 00000000000..972005724c7 --- /dev/null +++ b/ui/v2.5/src/components/Performers/PerformerCardGrid.tsx @@ -0,0 +1,39 @@ +import React, { useRef } from "react"; +import * as GQL from "src/core/generated-graphql"; +import { IPerformerCardExtraCriteria, PerformerCard } from "./PerformerCard"; +import { useContainerDimensions } from "../Shared/GridCard"; + +interface IPerformerCardGrid { + performers: GQL.PerformerDataFragment[]; + selectedIds: Set; + zoomIndex: number; + onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void; + extraCriteria?: IPerformerCardExtraCriteria; +} + +export const PerformerCardGrid: React.FC = ({ + performers, + selectedIds, + onSelectChange, + extraCriteria, +}) => { + const componentRef = useRef(null); + const { width } = useContainerDimensions(componentRef); + return ( +
+ {performers.map((p) => ( + 0} + selected={selectedIds.has(p.id)} + onSelectedChanged={(selected: boolean, shiftKey: boolean) => + onSelectChange(p.id, selected, shiftKey) + } + extraCriteria={extraCriteria} + /> + ))} +
+ ); +}; diff --git a/ui/v2.5/src/components/Performers/PerformerList.tsx b/ui/v2.5/src/components/Performers/PerformerList.tsx index a293ec4fbf8..257079bce9b 100644 --- a/ui/v2.5/src/components/Performers/PerformerList.tsx +++ b/ui/v2.5/src/components/Performers/PerformerList.tsx @@ -19,11 +19,12 @@ import { DisplayMode } from "src/models/list-filter/types"; import { PerformerTagger } from "../Tagger/performers/PerformerTagger"; import { ExportDialog } from "../Shared/ExportDialog"; import { DeleteEntityDialog } from "../Shared/DeleteEntityDialog"; -import { IPerformerCardExtraCriteria, PerformerCard } from "./PerformerCard"; +import { IPerformerCardExtraCriteria } from "./PerformerCard"; import { PerformerListTable } from "./PerformerListTable"; import { EditPerformersDialog } from "./EditPerformersDialog"; import { cmToImperial, cmToInches, kgToLbs } from "src/utils/units"; import TextUtils from "src/utils/text"; +import { PerformerCardGrid } from "./PerformerCardGrid"; const PerformerItemList = makeItemList({ filterMode: GQL.FilterMode.Performers, @@ -263,20 +264,13 @@ export const PerformerList: React.FC = ({ if (filter.displayMode === DisplayMode.Grid) { return ( -
- {result.data.findPerformers.performers.map((p) => ( - 0} - selected={selectedIds.has(p.id)} - onSelectedChanged={(selected: boolean, shiftKey: boolean) => - onSelectChange(p.id, selected, shiftKey) - } - extraCriteria={extraCriteria} - /> - ))} -
+ ); } if (filter.displayMode === DisplayMode.List) { diff --git a/ui/v2.5/src/components/Performers/styles.scss b/ui/v2.5/src/components/Performers/styles.scss index 05b75501502..55b11e07758 100644 --- a/ui/v2.5/src/components/Performers/styles.scss +++ b/ui/v2.5/src/components/Performers/styles.scss @@ -63,7 +63,7 @@ } &-image { - height: 30rem; + aspect-ratio: 2/3; min-width: 11.25rem; object-fit: cover; object-position: top; diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index 779c1202966..7103758feae 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -45,6 +45,7 @@ import airplay from "@silvermine/videojs-airplay"; // @ts-ignore import chromecast from "@silvermine/videojs-chromecast"; import abLoopPlugin from "videojs-abloop"; +import ScreenUtils from "src/utils/screen"; // register videojs plugins airplay(videojs); @@ -284,7 +285,7 @@ export const ScenePlayer: React.FC = ({ } const onResize = () => { - const show = window.innerHeight >= 450 && window.innerWidth >= 576; + const show = window.innerHeight >= 450 && !ScreenUtils.isMobile(); setShowScrubber(show); }; onResize(); diff --git a/ui/v2.5/src/components/Scenes/SceneCard.tsx b/ui/v2.5/src/components/Scenes/SceneCard.tsx index fc0591264dd..c385c9ea095 100644 --- a/ui/v2.5/src/components/Scenes/SceneCard.tsx +++ b/ui/v2.5/src/components/Scenes/SceneCard.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useRef } from "react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; import { Button, ButtonGroup, OverlayTrigger, Tooltip } from "react-bootstrap"; import { Link, useHistory } from "react-router-dom"; import cx from "classnames"; @@ -18,7 +18,7 @@ import TextUtils from "src/utils/text"; import { SceneQueue } from "src/models/sceneQueue"; import { ConfigurationContext } from "src/hooks/Config"; import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton"; -import { GridCard } from "../Shared/GridCard"; +import { GridCard, calculateCardWidth } from "../Shared/GridCard"; import { RatingBanner } from "../Shared/RatingBanner"; import { FormattedNumber } from "react-intl"; import { @@ -32,6 +32,7 @@ import { import { objectPath, objectTitle } from "src/core/files"; import { PreviewScrubber } from "./PreviewScrubber"; import { PatchComponent } from "src/pluginApi"; +import ScreenUtils from "src/utils/screen"; interface IScenePreviewProps { isPortrait: boolean; @@ -95,6 +96,8 @@ export const ScenePreview: React.FC = ({ interface ISceneCardProps { scene: GQL.SlimSceneDataFragment; + containerWidth?: number; + previewHeight?: number; index?: number; queue?: SceneQueue; compact?: boolean; @@ -461,6 +464,7 @@ export const SceneCard = PatchComponent( "SceneCard", (props: ISceneCardProps) => { const { configuration } = React.useContext(ConfigurationContext); + const [cardWidth, setCardWidth] = useState(); const file = useMemo( () => (props.scene.files.length > 0 ? props.scene.files[0] : undefined), @@ -483,6 +487,36 @@ export const SceneCard = PatchComponent( return ""; } + useEffect(() => { + if ( + !props.containerWidth || + props.zoomIndex === undefined || + ScreenUtils.isMobile() + ) + return; + + let zoomValue = props.zoomIndex; + let preferredCardWidth: number; + switch (zoomValue) { + case 0: + preferredCardWidth = 240; + break; + case 1: + preferredCardWidth = 340; // this value is intentionally higher than 320 + break; + case 2: + preferredCardWidth = 480; + break; + case 3: + preferredCardWidth = 640; + } + let fittedCardWidth = calculateCardWidth( + props.containerWidth, + preferredCardWidth! + ); + setCardWidth(fittedCardWidth); + }, [props, props.containerWidth, props.zoomIndex]); + const cont = configuration?.interface.continuePlaylistDefault ?? false; const sceneLink = props.queue @@ -497,6 +531,7 @@ export const SceneCard = PatchComponent( className={`scene-card ${zoomIndex()} ${filelessClass()}`} url={sceneLink} title={objectTitle(props.scene)} + width={cardWidth} linkClassName="scene-card-link" thumbnailSectionClassName="video-section" resumeTime={props.scene.resume_time ?? undefined} diff --git a/ui/v2.5/src/components/Scenes/SceneCardsGrid.tsx b/ui/v2.5/src/components/Scenes/SceneCardsGrid.tsx index 6852bf07625..2d21eda2bf1 100644 --- a/ui/v2.5/src/components/Scenes/SceneCardsGrid.tsx +++ b/ui/v2.5/src/components/Scenes/SceneCardsGrid.tsx @@ -1,7 +1,8 @@ -import React from "react"; +import React, { useRef } from "react"; import * as GQL from "src/core/generated-graphql"; import { SceneQueue } from "src/models/sceneQueue"; import { SceneCard } from "./SceneCard"; +import { useContainerDimensions } from "../Shared/GridCard"; interface ISceneCardsGrid { scenes: GQL.SlimSceneDataFragment[]; @@ -18,11 +19,14 @@ export const SceneCardsGrid: React.FC = ({ zoomIndex, onSelectChange, }) => { + const componentRef = useRef(null); + const { width } = useContainerDimensions(componentRef); return ( -
+
{scenes.map((scene, index) => ( { + const containerPadding = 30; + const cardMargin = 10; + let maxUsableWidth = containerWidth - containerPadding; + let maxElementsOnRow = Math.ceil(maxUsableWidth / preferredWidth); + return maxUsableWidth / maxElementsOnRow - cardMargin; +}; + +export const useContainerDimensions = ( + myRef: React.RefObject +) => { + const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); + + useEffect(() => { + const getDimensions = () => ({ + width: myRef.current!.offsetWidth, + height: myRef.current!.offsetHeight, + }); + + const handleResize = () => { + setDimensions(getDimensions()); + }; + + if (myRef.current) { + setDimensions(getDimensions()); + } + + window.addEventListener("resize", handleResize); + + return () => { + window.removeEventListener("resize", handleResize); + }; + }, [myRef]); + + return dimensions; +}; + export const GridCard: React.FC = (props: ICardProps) => { function handleImageClick(event: React.MouseEvent) { const { shiftKey } = event; @@ -116,6 +158,11 @@ export const GridCard: React.FC = (props: ICardProps) => { onDragStart={handleDrag} onDragOver={handleDragOver} draggable={props.onSelectedChanged && props.selecting} + style={ + props.width && !ScreenUtils.isMobile() + ? { width: `${props.width}px` } + : {} + } > {maybeRenderCheckbox()} diff --git a/ui/v2.5/src/components/Studios/StudioCard.tsx b/ui/v2.5/src/components/Studios/StudioCard.tsx index 1acae33ea4b..64dc52a137c 100644 --- a/ui/v2.5/src/components/Studios/StudioCard.tsx +++ b/ui/v2.5/src/components/Studios/StudioCard.tsx @@ -1,15 +1,17 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import * as GQL from "src/core/generated-graphql"; import NavUtils from "src/utils/navigation"; -import { GridCard } from "src/components/Shared/GridCard"; +import { GridCard, calculateCardWidth } from "src/components/Shared/GridCard"; import { ButtonGroup } from "react-bootstrap"; import { FormattedMessage } from "react-intl"; import { PopoverCountButton } from "../Shared/PopoverCountButton"; import { RatingBanner } from "../Shared/RatingBanner"; +import ScreenUtils from "src/utils/screen"; interface IProps { studio: GQL.StudioDataFragment; + containerWidth?: number; hideParent?: boolean; selecting?: boolean; selected?: boolean; @@ -59,11 +61,25 @@ function maybeRenderChildren(studio: GQL.StudioDataFragment) { export const StudioCard: React.FC = ({ studio, + containerWidth, hideParent, selecting, selected, onSelectedChanged, }) => { + const [cardWidth, setCardWidth] = useState(); + + useEffect(() => { + if (!containerWidth || ScreenUtils.isMobile()) return; + + let preferredCardWidth = 340; + let fittedCardWidth = calculateCardWidth( + containerWidth, + preferredCardWidth! + ); + setCardWidth(fittedCardWidth); + }, [containerWidth]); + function maybeRenderScenesPopoverButton() { if (!studio.scene_count) return; @@ -156,6 +172,7 @@ export const StudioCard: React.FC = ({ ; + onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void; +} + +export const StudioCardGrid: React.FC = ({ + studios, + fromParent, + selectedIds, + onSelectChange, +}) => { + const componentRef = useRef(null); + const { width } = useContainerDimensions(componentRef); + return ( +
+ {studios.map((studio) => ( + 0} + selected={selectedIds.has(studio.id)} + onSelectedChanged={(selected: boolean, shiftKey: boolean) => + onSelectChange(studio.id, selected, shiftKey) + } + /> + ))} +
+ ); +}; diff --git a/ui/v2.5/src/components/Studios/StudioList.tsx b/ui/v2.5/src/components/Studios/StudioList.tsx index 19710724b86..4e75c6405ed 100644 --- a/ui/v2.5/src/components/Studios/StudioList.tsx +++ b/ui/v2.5/src/components/Studios/StudioList.tsx @@ -18,8 +18,8 @@ import { ListFilterModel } from "src/models/list-filter/filter"; import { DisplayMode } from "src/models/list-filter/types"; import { ExportDialog } from "../Shared/ExportDialog"; import { DeleteEntityDialog } from "../Shared/DeleteEntityDialog"; -import { StudioCard } from "./StudioCard"; import { StudioTagger } from "../Tagger/studios/StudioTagger"; +import { StudioCardGrid } from "./StudioCardGrid"; const StudioItemList = makeItemList({ filterMode: GQL.FilterMode.Studios, @@ -135,20 +135,12 @@ export const StudioList: React.FC = ({ if (filter.displayMode === DisplayMode.Grid) { return ( -
- {result.data.findStudios.studios.map((studio) => ( - 0} - selected={selectedIds.has(studio.id)} - onSelectedChanged={(selected: boolean, shiftKey: boolean) => - onSelectChange(studio.id, selected, shiftKey) - } - /> - ))} -
+ ); } if (filter.displayMode === DisplayMode.List) { diff --git a/ui/v2.5/src/components/Tags/TagCard.tsx b/ui/v2.5/src/components/Tags/TagCard.tsx index 11c3c5223a8..9e651a4eadc 100644 --- a/ui/v2.5/src/components/Tags/TagCard.tsx +++ b/ui/v2.5/src/components/Tags/TagCard.tsx @@ -1,15 +1,17 @@ import { ButtonGroup } from "react-bootstrap"; -import React from "react"; +import React, { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import * as GQL from "src/core/generated-graphql"; import NavUtils from "src/utils/navigation"; import { FormattedMessage } from "react-intl"; import { TruncatedText } from "../Shared/TruncatedText"; -import { GridCard } from "../Shared/GridCard"; +import { GridCard, calculateCardWidth } from "../Shared/GridCard"; import { PopoverCountButton } from "../Shared/PopoverCountButton"; +import ScreenUtils from "src/utils/screen"; interface IProps { tag: GQL.TagDataFragment; + containerWidth?: number; zoomIndex: number; selecting?: boolean; selected?: boolean; @@ -18,11 +20,40 @@ interface IProps { export const TagCard: React.FC = ({ tag, + containerWidth, zoomIndex, selecting, selected, onSelectedChanged, }) => { + const [cardWidth, setCardWidth] = useState(); + + useEffect(() => { + if (!containerWidth || zoomIndex === undefined || ScreenUtils.isMobile()) + return; + + let zoomValue = zoomIndex; + let preferredCardWidth: number; + switch (zoomValue) { + case 0: + preferredCardWidth = 240; + break; + case 1: + preferredCardWidth = 340; + break; + case 2: + preferredCardWidth = 480; + break; + case 3: + preferredCardWidth = 640; + } + let fittedCardWidth = calculateCardWidth( + containerWidth, + preferredCardWidth! + ); + setCardWidth(fittedCardWidth); + }, [containerWidth, zoomIndex]); + function maybeRenderDescription() { if (tag.description) { return ( @@ -181,6 +212,7 @@ export const TagCard: React.FC = ({ ; + zoomIndex: number; + onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void; +} + +export const TagCardGrid: React.FC = ({ + tags, + selectedIds, + zoomIndex, + onSelectChange, +}) => { + const componentRef = useRef(null); + const { width } = useContainerDimensions(componentRef); + return ( +
+ {tags.map((tag) => ( + 0} + selected={selectedIds.has(tag.id)} + onSelectedChanged={(selected: boolean, shiftKey: boolean) => + onSelectChange(tag.id, selected, shiftKey) + } + /> + ))} +
+ ); +}; diff --git a/ui/v2.5/src/components/Tags/TagList.tsx b/ui/v2.5/src/components/Tags/TagList.tsx index 869282418c0..ea580c2c948 100644 --- a/ui/v2.5/src/components/Tags/TagList.tsx +++ b/ui/v2.5/src/components/Tags/TagList.tsx @@ -24,10 +24,10 @@ import NavUtils from "src/utils/navigation"; import { Icon } from "../Shared/Icon"; import { ModalComponent } from "../Shared/Modal"; import { DeleteEntityDialog } from "../Shared/DeleteEntityDialog"; -import { TagCard } from "./TagCard"; import { ExportDialog } from "../Shared/ExportDialog"; import { tagRelationHook } from "../../core/tags"; import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; +import { TagCardGrid } from "./TagCardGrid"; interface ITagList { filterHook?: (filter: ListFilterModel) => ListFilterModel; @@ -188,20 +188,12 @@ export const TagList: React.FC = ({ filterHook, alterQuery }) => { if (filter.displayMode === DisplayMode.Grid) { return ( -
- {result.data.findTags.tags.map((tag) => ( - 0} - selected={selectedIds.has(tag.id)} - onSelectedChanged={(selected: boolean, shiftKey: boolean) => - onSelectChange(tag.id, selected, shiftKey) - } - /> - ))} -
+ ); } if (filter.displayMode === DisplayMode.List) { diff --git a/ui/v2.5/src/index.scss b/ui/v2.5/src/index.scss index 60ec8bbf163..a86e7f7a5fc 100755 --- a/ui/v2.5/src/index.scss +++ b/ui/v2.5/src/index.scss @@ -487,16 +487,6 @@ textarea.text-input { } .zoom-0 { - width: 240px; - - .scene-card-preview { - height: 135px; - } - - .portrait { - height: 180px; - } - .gallery-card-image, .tag-card-image { max-height: 180px; @@ -506,14 +496,6 @@ textarea.text-input { .zoom-1 { width: 320px; - .scene-card-preview { - height: 180px; - } - - .portrait { - height: 240px; - } - .gallery-card-image, .tag-card-image { max-height: 240px; @@ -525,16 +507,6 @@ textarea.text-input { } .zoom-2 { - width: 480px; - - .scene-card-preview { - height: 270px; - } - - .portrait { - height: 360px; - } - .gallery-card-image, .tag-card-image { max-height: 360px; @@ -546,16 +518,6 @@ textarea.text-input { } .zoom-3 { - width: 640px; - - .scene-card-preview { - height: 360px; - } - - .portrait { - height: 480px; - } - .tag-card-image, .gallery-card-image { max-height: 480px; diff --git a/ui/v2.5/src/utils/screen.ts b/ui/v2.5/src/utils/screen.ts index 16b13feb4ed..b800be86b96 100644 --- a/ui/v2.5/src/utils/screen.ts +++ b/ui/v2.5/src/utils/screen.ts @@ -1,5 +1,5 @@ const isMobile = () => - window.matchMedia("only screen and (max-width: 767px)").matches; + window.matchMedia("only screen and (max-width: 576px)").matches; const ScreenUtils = { isMobile,