Fit cards properly within their containers (#4514)

* created missing cards grids
This commit is contained in:
CJ
2024-02-05 20:06:47 -06:00
committed by GitHub
parent 330581283a
commit a8df95c3a4
25 changed files with 460 additions and 124 deletions

View File

@@ -319,10 +319,6 @@
.slick-list .performer-card.card { .slick-list .performer-card.card {
width: 16rem; width: 16rem;
} }
.slick-list .performer-card-image {
height: 24rem;
}
} }
/* Icons */ /* Icons */

View File

@@ -1,8 +1,8 @@
import { Button, ButtonGroup, OverlayTrigger, Tooltip } from "react-bootstrap"; 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 { Link } from "react-router-dom";
import * as GQL from "src/core/generated-graphql"; 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 { HoverPopover } from "../Shared/HoverPopover";
import { Icon } from "../Shared/Icon"; import { Icon } from "../Shared/Icon";
import { SceneLink, TagLink } from "../Shared/TagLink"; import { SceneLink, TagLink } from "../Shared/TagLink";
@@ -14,9 +14,11 @@ import { ConfigurationContext } from "src/hooks/Config";
import { RatingBanner } from "../Shared/RatingBanner"; import { RatingBanner } from "../Shared/RatingBanner";
import { faBox, faPlayCircle, faTag } from "@fortawesome/free-solid-svg-icons"; import { faBox, faPlayCircle, faTag } from "@fortawesome/free-solid-svg-icons";
import { galleryTitle } from "src/core/galleries"; import { galleryTitle } from "src/core/galleries";
import ScreenUtils from "src/utils/screen";
interface IProps { interface IProps {
gallery: GQL.SlimGalleryDataFragment; gallery: GQL.SlimGalleryDataFragment;
containerWidth?: number;
selecting?: boolean; selecting?: boolean;
selected?: boolean | undefined; selected?: boolean | undefined;
zoomIndex?: number; zoomIndex?: number;
@@ -26,6 +28,37 @@ interface IProps {
export const GalleryCard: React.FC<IProps> = (props) => { export const GalleryCard: React.FC<IProps> = (props) => {
const { configuration } = React.useContext(ConfigurationContext); const { configuration } = React.useContext(ConfigurationContext);
const showStudioAsText = configuration?.interface.showStudioAsText ?? false; const showStudioAsText = configuration?.interface.showStudioAsText ?? false;
const [cardWidth, setCardWidth] = useState<number>();
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() { function maybeRenderScenePopoverButton() {
if (props.gallery.scenes.length === 0) return; if (props.gallery.scenes.length === 0) return;
@@ -153,6 +186,7 @@ export const GalleryCard: React.FC<IProps> = (props) => {
<GridCard <GridCard
className={`gallery-card zoom-${props.zoomIndex}`} className={`gallery-card zoom-${props.zoomIndex}`}
url={`/galleries/${props.gallery.id}`} url={`/galleries/${props.gallery.id}`}
width={cardWidth}
title={galleryTitle(props.gallery)} title={galleryTitle(props.gallery)}
linkClassName="gallery-card-header" linkClassName="gallery-card-header"
image={ image={

View File

@@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useRef, useState } from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import cloneDeep from "lodash-es/cloneDeep"; import cloneDeep from "lodash-es/cloneDeep";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
@@ -18,6 +18,7 @@ import { EditGalleriesDialog } from "./EditGalleriesDialog";
import { DeleteGalleriesDialog } from "./DeleteGalleriesDialog"; import { DeleteGalleriesDialog } from "./DeleteGalleriesDialog";
import { ExportDialog } from "../Shared/ExportDialog"; import { ExportDialog } from "../Shared/ExportDialog";
import { GalleryListTable } from "./GalleryListTable"; import { GalleryListTable } from "./GalleryListTable";
import { useContainerDimensions } from "../Shared/GridCard";
const GalleryItemList = makeItemList({ const GalleryItemList = makeItemList({
filterMode: GQL.FilterMode.Galleries, filterMode: GQL.FilterMode.Galleries,
@@ -106,6 +107,9 @@ export const GalleryList: React.FC<IGalleryList> = ({
setIsExportDialogOpen(true); setIsExportDialogOpen(true);
} }
const componentRef = useRef<HTMLDivElement>(null);
const { width } = useContainerDimensions(componentRef);
function renderContent( function renderContent(
result: GQL.FindGalleriesQueryResult, result: GQL.FindGalleriesQueryResult,
filter: ListFilterModel, filter: ListFilterModel,
@@ -133,10 +137,11 @@ export const GalleryList: React.FC<IGalleryList> = ({
if (filter.displayMode === DisplayMode.Grid) { if (filter.displayMode === DisplayMode.Grid) {
return ( return (
<div className="row justify-content-center"> <div className="row justify-content-center" ref={componentRef}>
{result.data.findGalleries.galleries.map((gallery) => ( {result.data.findGalleries.galleries.map((gallery) => (
<GalleryCard <GalleryCard
key={gallery.id} key={gallery.id}
containerWidth={width}
gallery={gallery} gallery={gallery}
zoomIndex={filter.zoomIndex} zoomIndex={filter.zoomIndex}
selecting={selectedIds.size > 0} selecting={selectedIds.size > 0}

View File

@@ -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 { Button, ButtonGroup } from "react-bootstrap";
import cx from "classnames"; import cx from "classnames";
import * as GQL from "src/core/generated-graphql"; 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 { HoverPopover } from "src/components/Shared/HoverPopover";
import { SweatDrops } from "src/components/Shared/SweatDrops"; import { SweatDrops } from "src/components/Shared/SweatDrops";
import { PerformerPopoverButton } from "src/components/Shared/PerformerPopoverButton"; 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 { RatingBanner } from "src/components/Shared/RatingBanner";
import { import {
faBox, faBox,
@@ -17,9 +17,11 @@ import {
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { objectTitle } from "src/core/files"; import { objectTitle } from "src/core/files";
import { TruncatedText } from "../Shared/TruncatedText"; import { TruncatedText } from "../Shared/TruncatedText";
import ScreenUtils from "src/utils/screen";
interface IImageCardProps { interface IImageCardProps {
image: GQL.SlimImageDataFragment; image: GQL.SlimImageDataFragment;
containerWidth?: number;
selecting?: boolean; selecting?: boolean;
selected?: boolean | undefined; selected?: boolean | undefined;
zoomIndex: number; zoomIndex: number;
@@ -30,6 +32,38 @@ interface IImageCardProps {
export const ImageCard: React.FC<IImageCardProps> = ( export const ImageCard: React.FC<IImageCardProps> = (
props: IImageCardProps props: IImageCardProps
) => { ) => {
const [cardWidth, setCardWidth] = useState<number>();
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( const file = useMemo(
() => () =>
props.image.visual_files.length > 0 props.image.visual_files.length > 0
@@ -153,6 +187,7 @@ export const ImageCard: React.FC<IImageCardProps> = (
<GridCard <GridCard
className={`image-card zoom-${props.zoomIndex}`} className={`image-card zoom-${props.zoomIndex}`}
url={`/images/${props.image.id}`} url={`/images/${props.image.id}`}
width={cardWidth}
title={objectTitle(props.image)} title={objectTitle(props.image)}
linkClassName="image-card-link" linkClassName="image-card-link"
image={ image={

View File

@@ -4,6 +4,7 @@ import React, {
useMemo, useMemo,
MouseEvent, MouseEvent,
useContext, useContext,
useRef,
} from "react"; } from "react";
import { FormattedNumber, useIntl } from "react-intl"; import { FormattedNumber, useIntl } from "react-intl";
import cloneDeep from "lodash-es/cloneDeep"; import cloneDeep from "lodash-es/cloneDeep";
@@ -32,6 +33,7 @@ import { objectTitle } from "src/core/files";
import TextUtils from "src/utils/text"; import TextUtils from "src/utils/text";
import { ConfigurationContext } from "src/hooks/Config"; import { ConfigurationContext } from "src/hooks/Config";
import { IUIConfig } from "src/core/config"; import { IUIConfig } from "src/core/config";
import { useContainerDimensions } from "../Shared/GridCard";
interface IImageWallProps { interface IImageWallProps {
images: GQL.SlimImageDataFragment[]; images: GQL.SlimImageDataFragment[];
@@ -196,6 +198,9 @@ const ImageListImages: React.FC<IImageListImages> = ({
ev.preventDefault(); ev.preventDefault();
} }
const componentRef = useRef<HTMLDivElement>(null);
const { width } = useContainerDimensions(componentRef);
function renderImageCard( function renderImageCard(
index: number, index: number,
image: GQL.SlimImageDataFragment, image: GQL.SlimImageDataFragment,
@@ -204,6 +209,7 @@ const ImageListImages: React.FC<IImageListImages> = ({
return ( return (
<ImageCard <ImageCard
key={image.id} key={image.id}
containerWidth={width}
image={image} image={image}
zoomIndex={zoomIndex} zoomIndex={zoomIndex}
selecting={selectedIds.size > 0} selecting={selectedIds.size > 0}
@@ -220,7 +226,7 @@ const ImageListImages: React.FC<IImageListImages> = ({
if (filter.displayMode === DisplayMode.Grid) { if (filter.displayMode === DisplayMode.Grid) {
return ( return (
<div className="row justify-content-center"> <div className="row justify-content-center" ref={componentRef}>
{images.map((image, index) => {images.map((image, index) =>
renderImageCard(index, image, filter.zoomIndex) renderImageCard(index, image, filter.zoomIndex)
)} )}

View File

@@ -1,7 +1,7 @@
import React from "react"; import React, { useEffect, useState } from "react";
import { Button, ButtonGroup } from "react-bootstrap"; import { Button, ButtonGroup } from "react-bootstrap";
import * as GQL from "src/core/generated-graphql"; 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 { HoverPopover } from "../Shared/HoverPopover";
import { Icon } from "../Shared/Icon"; import { Icon } from "../Shared/Icon";
import { SceneLink } from "../Shared/TagLink"; import { SceneLink } from "../Shared/TagLink";
@@ -9,9 +9,11 @@ import { TruncatedText } from "../Shared/TruncatedText";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { RatingBanner } from "../Shared/RatingBanner"; import { RatingBanner } from "../Shared/RatingBanner";
import { faPlayCircle } from "@fortawesome/free-solid-svg-icons"; import { faPlayCircle } from "@fortawesome/free-solid-svg-icons";
import ScreenUtils from "src/utils/screen";
interface IProps { interface IProps {
movie: GQL.MovieDataFragment; movie: GQL.MovieDataFragment;
containerWidth?: number;
sceneIndex?: number; sceneIndex?: number;
selecting?: boolean; selecting?: boolean;
selected?: boolean; selected?: boolean;
@@ -19,6 +21,19 @@ interface IProps {
} }
export const MovieCard: React.FC<IProps> = (props: IProps) => { export const MovieCard: React.FC<IProps> = (props: IProps) => {
const [cardWidth, setCardWidth] = useState<number>();
useEffect(() => {
if (!props.containerWidth || ScreenUtils.isMobile()) return;
let preferredCardWidth = 250;
let fittedCardWidth = calculateCardWidth(
props.containerWidth,
preferredCardWidth!
);
setCardWidth(fittedCardWidth);
}, [props, props.containerWidth]);
function maybeRenderSceneNumber() { function maybeRenderSceneNumber() {
if (!props.sceneIndex) return; if (!props.sceneIndex) return;
@@ -71,6 +86,7 @@ export const MovieCard: React.FC<IProps> = (props: IProps) => {
<GridCard <GridCard
className="movie-card" className="movie-card"
url={`/movies/${props.movie.id}`} url={`/movies/${props.movie.id}`}
width={cardWidth}
title={props.movie.name} title={props.movie.name}
linkClassName="movie-card-header" linkClassName="movie-card-header"
image={ image={

View File

@@ -0,0 +1,35 @@
import React, { useRef } from "react";
import * as GQL from "src/core/generated-graphql";
import { MovieCard } from "./MovieCard";
import { useContainerDimensions } from "../Shared/GridCard";
interface IMovieCardGrid {
movies: GQL.MovieDataFragment[];
selectedIds: Set<string>;
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void;
}
export const MovieCardGrid: React.FC<IMovieCardGrid> = ({
movies,
selectedIds,
onSelectChange,
}) => {
const componentRef = useRef<HTMLDivElement>(null);
const { width } = useContainerDimensions(componentRef);
return (
<div className="row justify-content-center" ref={componentRef}>
{movies.map((p) => (
<MovieCard
key={p.id}
containerWidth={width}
movie={p}
selecting={selectedIds.size > 0}
selected={selectedIds.has(p.id)}
onSelectedChanged={(selected: boolean, shiftKey: boolean) =>
onSelectChange(p.id, selected, shiftKey)
}
/>
))}
</div>
);
};

View File

@@ -18,7 +18,7 @@ import {
} from "../List/ItemList"; } from "../List/ItemList";
import { ExportDialog } from "../Shared/ExportDialog"; import { ExportDialog } from "../Shared/ExportDialog";
import { DeleteEntityDialog } from "../Shared/DeleteEntityDialog"; import { DeleteEntityDialog } from "../Shared/DeleteEntityDialog";
import { MovieCard } from "./MovieCard"; import { MovieCardGrid } from "./MovieCardGrid";
import { EditMoviesDialog } from "./EditMoviesDialog"; import { EditMoviesDialog } from "./EditMoviesDialog";
const MovieItemList = makeItemList({ const MovieItemList = makeItemList({
@@ -130,19 +130,11 @@ export const MovieList: React.FC<IMovieList> = ({ filterHook, alterQuery }) => {
if (filter.displayMode === DisplayMode.Grid) { if (filter.displayMode === DisplayMode.Grid) {
return ( return (
<div className="row justify-content-center"> <MovieCardGrid
{result.data.findMovies.movies.map((p) => ( movies={result.data.findMovies.movies}
<MovieCard selectedIds={selectedIds}
key={p.id} onSelectChange={onSelectChange}
movie={p} />
selecting={selectedIds.size > 0}
selected={selectedIds.has(p.id)}
onSelectedChanged={(selected: boolean, shiftKey: boolean) =>
onSelectChange(p.id, selected, shiftKey)
}
/>
))}
</div>
); );
} }
if (filter.displayMode === DisplayMode.List) { if (filter.displayMode === DisplayMode.List) {

View File

@@ -1,10 +1,10 @@
import React from "react"; import React, { useEffect, useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import NavUtils from "src/utils/navigation"; import NavUtils from "src/utils/navigation";
import TextUtils from "src/utils/text"; import TextUtils from "src/utils/text";
import { GridCard } from "../Shared/GridCard"; import { GridCard, calculateCardWidth } from "../Shared/GridCard";
import { CountryFlag } from "../Shared/CountryFlag"; import { CountryFlag } from "../Shared/CountryFlag";
import { SweatDrops } from "../Shared/SweatDrops"; import { SweatDrops } from "../Shared/SweatDrops";
import { HoverPopover } from "../Shared/HoverPopover"; import { HoverPopover } from "../Shared/HoverPopover";
@@ -22,6 +22,7 @@ import { RatingBanner } from "../Shared/RatingBanner";
import cx from "classnames"; import cx from "classnames";
import { usePerformerUpdate } from "src/core/StashService"; import { usePerformerUpdate } from "src/core/StashService";
import { ILabeledId } from "src/models/list-filter/types"; import { ILabeledId } from "src/models/list-filter/types";
import ScreenUtils from "src/utils/screen";
export interface IPerformerCardExtraCriteria { export interface IPerformerCardExtraCriteria {
scenes?: Criterion<CriterionValue>[]; scenes?: Criterion<CriterionValue>[];
@@ -33,6 +34,7 @@ export interface IPerformerCardExtraCriteria {
interface IPerformerCardProps { interface IPerformerCardProps {
performer: GQL.PerformerDataFragment; performer: GQL.PerformerDataFragment;
containerWidth?: number;
ageFromDate?: string; ageFromDate?: string;
selecting?: boolean; selecting?: boolean;
selected?: boolean; selected?: boolean;
@@ -42,6 +44,7 @@ interface IPerformerCardProps {
export const PerformerCard: React.FC<IPerformerCardProps> = ({ export const PerformerCard: React.FC<IPerformerCardProps> = ({
performer, performer,
containerWidth,
ageFromDate, ageFromDate,
selecting, selecting,
selected, selected,
@@ -66,6 +69,18 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
); );
const [updatePerformer] = usePerformerUpdate(); const [updatePerformer] = usePerformerUpdate();
const [cardWidth, setCardWidth] = useState<number>();
useEffect(() => {
if (!containerWidth || ScreenUtils.isMobile()) return;
let preferredCardWidth = 300;
let fittedCardWidth = calculateCardWidth(
containerWidth,
preferredCardWidth!
);
setCardWidth(fittedCardWidth);
}, [containerWidth]);
function renderFavoriteIcon() { function renderFavoriteIcon() {
return ( return (
@@ -251,6 +266,7 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
<GridCard <GridCard
className="performer-card" className="performer-card"
url={`/performers/${performer.id}`} url={`/performers/${performer.id}`}
width={cardWidth}
pretitleIcon={ pretitleIcon={
<GenderIcon className="gender-icon" gender={performer.gender} /> <GenderIcon className="gender-icon" gender={performer.gender} />
} }

View File

@@ -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<string>;
zoomIndex: number;
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void;
extraCriteria?: IPerformerCardExtraCriteria;
}
export const PerformerCardGrid: React.FC<IPerformerCardGrid> = ({
performers,
selectedIds,
onSelectChange,
extraCriteria,
}) => {
const componentRef = useRef<HTMLDivElement>(null);
const { width } = useContainerDimensions(componentRef);
return (
<div className="row justify-content-center" ref={componentRef}>
{performers.map((p) => (
<PerformerCard
key={p.id}
containerWidth={width}
performer={p}
selecting={selectedIds.size > 0}
selected={selectedIds.has(p.id)}
onSelectedChanged={(selected: boolean, shiftKey: boolean) =>
onSelectChange(p.id, selected, shiftKey)
}
extraCriteria={extraCriteria}
/>
))}
</div>
);
};

View File

@@ -19,11 +19,12 @@ import { DisplayMode } from "src/models/list-filter/types";
import { PerformerTagger } from "../Tagger/performers/PerformerTagger"; import { PerformerTagger } from "../Tagger/performers/PerformerTagger";
import { ExportDialog } from "../Shared/ExportDialog"; import { ExportDialog } from "../Shared/ExportDialog";
import { DeleteEntityDialog } from "../Shared/DeleteEntityDialog"; import { DeleteEntityDialog } from "../Shared/DeleteEntityDialog";
import { IPerformerCardExtraCriteria, PerformerCard } from "./PerformerCard"; import { IPerformerCardExtraCriteria } from "./PerformerCard";
import { PerformerListTable } from "./PerformerListTable"; import { PerformerListTable } from "./PerformerListTable";
import { EditPerformersDialog } from "./EditPerformersDialog"; import { EditPerformersDialog } from "./EditPerformersDialog";
import { cmToImperial, cmToInches, kgToLbs } from "src/utils/units"; import { cmToImperial, cmToInches, kgToLbs } from "src/utils/units";
import TextUtils from "src/utils/text"; import TextUtils from "src/utils/text";
import { PerformerCardGrid } from "./PerformerCardGrid";
const PerformerItemList = makeItemList({ const PerformerItemList = makeItemList({
filterMode: GQL.FilterMode.Performers, filterMode: GQL.FilterMode.Performers,
@@ -263,20 +264,13 @@ export const PerformerList: React.FC<IPerformerList> = ({
if (filter.displayMode === DisplayMode.Grid) { if (filter.displayMode === DisplayMode.Grid) {
return ( return (
<div className="row justify-content-center"> <PerformerCardGrid
{result.data.findPerformers.performers.map((p) => ( performers={result.data.findPerformers.performers}
<PerformerCard zoomIndex={filter.zoomIndex}
key={p.id} selectedIds={selectedIds}
performer={p} onSelectChange={onSelectChange}
selecting={selectedIds.size > 0} extraCriteria={extraCriteria}
selected={selectedIds.has(p.id)} />
onSelectedChanged={(selected: boolean, shiftKey: boolean) =>
onSelectChange(p.id, selected, shiftKey)
}
extraCriteria={extraCriteria}
/>
))}
</div>
); );
} }
if (filter.displayMode === DisplayMode.List) { if (filter.displayMode === DisplayMode.List) {

View File

@@ -63,7 +63,7 @@
} }
&-image { &-image {
height: 30rem; aspect-ratio: 2/3;
min-width: 11.25rem; min-width: 11.25rem;
object-fit: cover; object-fit: cover;
object-position: top; object-position: top;

View File

@@ -45,6 +45,7 @@ import airplay from "@silvermine/videojs-airplay";
// @ts-ignore // @ts-ignore
import chromecast from "@silvermine/videojs-chromecast"; import chromecast from "@silvermine/videojs-chromecast";
import abLoopPlugin from "videojs-abloop"; import abLoopPlugin from "videojs-abloop";
import ScreenUtils from "src/utils/screen";
// register videojs plugins // register videojs plugins
airplay(videojs); airplay(videojs);
@@ -284,7 +285,7 @@ export const ScenePlayer: React.FC<IScenePlayerProps> = ({
} }
const onResize = () => { const onResize = () => {
const show = window.innerHeight >= 450 && window.innerWidth >= 576; const show = window.innerHeight >= 450 && !ScreenUtils.isMobile();
setShowScrubber(show); setShowScrubber(show);
}; };
onResize(); onResize();

View File

@@ -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 { Button, ButtonGroup, OverlayTrigger, Tooltip } from "react-bootstrap";
import { Link, useHistory } from "react-router-dom"; import { Link, useHistory } from "react-router-dom";
import cx from "classnames"; import cx from "classnames";
@@ -18,7 +18,7 @@ import TextUtils from "src/utils/text";
import { SceneQueue } from "src/models/sceneQueue"; import { SceneQueue } from "src/models/sceneQueue";
import { ConfigurationContext } from "src/hooks/Config"; import { ConfigurationContext } from "src/hooks/Config";
import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton"; import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton";
import { GridCard } from "../Shared/GridCard"; import { GridCard, calculateCardWidth } from "../Shared/GridCard";
import { RatingBanner } from "../Shared/RatingBanner"; import { RatingBanner } from "../Shared/RatingBanner";
import { FormattedNumber } from "react-intl"; import { FormattedNumber } from "react-intl";
import { import {
@@ -32,6 +32,7 @@ import {
import { objectPath, objectTitle } from "src/core/files"; import { objectPath, objectTitle } from "src/core/files";
import { PreviewScrubber } from "./PreviewScrubber"; import { PreviewScrubber } from "./PreviewScrubber";
import { PatchComponent } from "src/pluginApi"; import { PatchComponent } from "src/pluginApi";
import ScreenUtils from "src/utils/screen";
interface IScenePreviewProps { interface IScenePreviewProps {
isPortrait: boolean; isPortrait: boolean;
@@ -95,6 +96,8 @@ export const ScenePreview: React.FC<IScenePreviewProps> = ({
interface ISceneCardProps { interface ISceneCardProps {
scene: GQL.SlimSceneDataFragment; scene: GQL.SlimSceneDataFragment;
containerWidth?: number;
previewHeight?: number;
index?: number; index?: number;
queue?: SceneQueue; queue?: SceneQueue;
compact?: boolean; compact?: boolean;
@@ -461,6 +464,7 @@ export const SceneCard = PatchComponent(
"SceneCard", "SceneCard",
(props: ISceneCardProps) => { (props: ISceneCardProps) => {
const { configuration } = React.useContext(ConfigurationContext); const { configuration } = React.useContext(ConfigurationContext);
const [cardWidth, setCardWidth] = useState<number>();
const file = useMemo( const file = useMemo(
() => (props.scene.files.length > 0 ? props.scene.files[0] : undefined), () => (props.scene.files.length > 0 ? props.scene.files[0] : undefined),
@@ -483,6 +487,36 @@ export const SceneCard = PatchComponent(
return ""; 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 cont = configuration?.interface.continuePlaylistDefault ?? false;
const sceneLink = props.queue const sceneLink = props.queue
@@ -497,6 +531,7 @@ export const SceneCard = PatchComponent(
className={`scene-card ${zoomIndex()} ${filelessClass()}`} className={`scene-card ${zoomIndex()} ${filelessClass()}`}
url={sceneLink} url={sceneLink}
title={objectTitle(props.scene)} title={objectTitle(props.scene)}
width={cardWidth}
linkClassName="scene-card-link" linkClassName="scene-card-link"
thumbnailSectionClassName="video-section" thumbnailSectionClassName="video-section"
resumeTime={props.scene.resume_time ?? undefined} resumeTime={props.scene.resume_time ?? undefined}

View File

@@ -1,7 +1,8 @@
import React from "react"; import React, { useRef } from "react";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { SceneQueue } from "src/models/sceneQueue"; import { SceneQueue } from "src/models/sceneQueue";
import { SceneCard } from "./SceneCard"; import { SceneCard } from "./SceneCard";
import { useContainerDimensions } from "../Shared/GridCard";
interface ISceneCardsGrid { interface ISceneCardsGrid {
scenes: GQL.SlimSceneDataFragment[]; scenes: GQL.SlimSceneDataFragment[];
@@ -18,11 +19,14 @@ export const SceneCardsGrid: React.FC<ISceneCardsGrid> = ({
zoomIndex, zoomIndex,
onSelectChange, onSelectChange,
}) => { }) => {
const componentRef = useRef<HTMLDivElement>(null);
const { width } = useContainerDimensions(componentRef);
return ( return (
<div className="row justify-content-center"> <div className="row justify-content-center" ref={componentRef}>
{scenes.map((scene, index) => ( {scenes.map((scene, index) => (
<SceneCard <SceneCard
key={scene.id} key={scene.id}
containerWidth={width}
scene={scene} scene={scene}
queue={queue} queue={queue}
index={index} index={index}

View File

@@ -82,6 +82,10 @@ textarea.scene-description {
} }
} }
.justify-content-center .studio-card .studio-card-image {
width: 100%;
}
.studio-card { .studio-card {
padding: 0.5rem; padding: 0.5rem;
@@ -164,6 +168,12 @@ textarea.scene-description {
text-transform: uppercase; text-transform: uppercase;
} }
.scene-card {
&-preview {
aspect-ratio: 16/9;
}
}
.scene-card, .scene-card,
.gallery-card { .gallery-card {
a { a {

View File

@@ -1,13 +1,15 @@
import React from "react"; import React, { useEffect, useState } from "react";
import { Card, Form } from "react-bootstrap"; import { Card, Form } from "react-bootstrap";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import cx from "classnames"; import cx from "classnames";
import { TruncatedText } from "./TruncatedText"; import { TruncatedText } from "./TruncatedText";
import ScreenUtils from "src/utils/screen";
interface ICardProps { interface ICardProps {
className?: string; className?: string;
linkClassName?: string; linkClassName?: string;
thumbnailSectionClassName?: string; thumbnailSectionClassName?: string;
width?: number;
url: string; url: string;
pretitleIcon?: JSX.Element; pretitleIcon?: JSX.Element;
title: JSX.Element | string; title: JSX.Element | string;
@@ -23,6 +25,46 @@ interface ICardProps {
interactiveHeatmap?: string; interactiveHeatmap?: string;
} }
export const calculateCardWidth = (
containerWidth: number,
preferredWidth: number
) => {
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<HTMLDivElement>
) => {
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<ICardProps> = (props: ICardProps) => { export const GridCard: React.FC<ICardProps> = (props: ICardProps) => {
function handleImageClick(event: React.MouseEvent<HTMLElement, MouseEvent>) { function handleImageClick(event: React.MouseEvent<HTMLElement, MouseEvent>) {
const { shiftKey } = event; const { shiftKey } = event;
@@ -116,6 +158,11 @@ export const GridCard: React.FC<ICardProps> = (props: ICardProps) => {
onDragStart={handleDrag} onDragStart={handleDrag}
onDragOver={handleDragOver} onDragOver={handleDragOver}
draggable={props.onSelectedChanged && props.selecting} draggable={props.onSelectedChanged && props.selecting}
style={
props.width && !ScreenUtils.isMobile()
? { width: `${props.width}px` }
: {}
}
> >
{maybeRenderCheckbox()} {maybeRenderCheckbox()}

View File

@@ -1,15 +1,17 @@
import React from "react"; import React, { useEffect, useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import NavUtils from "src/utils/navigation"; 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 { ButtonGroup } from "react-bootstrap";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { PopoverCountButton } from "../Shared/PopoverCountButton"; import { PopoverCountButton } from "../Shared/PopoverCountButton";
import { RatingBanner } from "../Shared/RatingBanner"; import { RatingBanner } from "../Shared/RatingBanner";
import ScreenUtils from "src/utils/screen";
interface IProps { interface IProps {
studio: GQL.StudioDataFragment; studio: GQL.StudioDataFragment;
containerWidth?: number;
hideParent?: boolean; hideParent?: boolean;
selecting?: boolean; selecting?: boolean;
selected?: boolean; selected?: boolean;
@@ -59,11 +61,25 @@ function maybeRenderChildren(studio: GQL.StudioDataFragment) {
export const StudioCard: React.FC<IProps> = ({ export const StudioCard: React.FC<IProps> = ({
studio, studio,
containerWidth,
hideParent, hideParent,
selecting, selecting,
selected, selected,
onSelectedChanged, onSelectedChanged,
}) => { }) => {
const [cardWidth, setCardWidth] = useState<number>();
useEffect(() => {
if (!containerWidth || ScreenUtils.isMobile()) return;
let preferredCardWidth = 340;
let fittedCardWidth = calculateCardWidth(
containerWidth,
preferredCardWidth!
);
setCardWidth(fittedCardWidth);
}, [containerWidth]);
function maybeRenderScenesPopoverButton() { function maybeRenderScenesPopoverButton() {
if (!studio.scene_count) return; if (!studio.scene_count) return;
@@ -156,6 +172,7 @@ export const StudioCard: React.FC<IProps> = ({
<GridCard <GridCard
className="studio-card" className="studio-card"
url={`/studios/${studio.id}`} url={`/studios/${studio.id}`}
width={cardWidth}
title={studio.name} title={studio.name}
linkClassName="studio-card-header" linkClassName="studio-card-header"
image={ image={

View File

@@ -0,0 +1,38 @@
import React, { useRef } from "react";
import * as GQL from "src/core/generated-graphql";
import { useContainerDimensions } from "../Shared/GridCard";
import { StudioCard } from "./StudioCard";
interface IStudioCardGrid {
studios: GQL.StudioDataFragment[];
fromParent: boolean | undefined;
selectedIds: Set<string>;
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void;
}
export const StudioCardGrid: React.FC<IStudioCardGrid> = ({
studios,
fromParent,
selectedIds,
onSelectChange,
}) => {
const componentRef = useRef<HTMLDivElement>(null);
const { width } = useContainerDimensions(componentRef);
return (
<div className="row justify-content-center" ref={componentRef}>
{studios.map((studio) => (
<StudioCard
key={studio.id}
containerWidth={width}
studio={studio}
hideParent={fromParent}
selecting={selectedIds.size > 0}
selected={selectedIds.has(studio.id)}
onSelectedChanged={(selected: boolean, shiftKey: boolean) =>
onSelectChange(studio.id, selected, shiftKey)
}
/>
))}
</div>
);
};

View File

@@ -18,8 +18,8 @@ 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 { ExportDialog } from "../Shared/ExportDialog"; import { ExportDialog } from "../Shared/ExportDialog";
import { DeleteEntityDialog } from "../Shared/DeleteEntityDialog"; import { DeleteEntityDialog } from "../Shared/DeleteEntityDialog";
import { StudioCard } from "./StudioCard";
import { StudioTagger } from "../Tagger/studios/StudioTagger"; import { StudioTagger } from "../Tagger/studios/StudioTagger";
import { StudioCardGrid } from "./StudioCardGrid";
const StudioItemList = makeItemList({ const StudioItemList = makeItemList({
filterMode: GQL.FilterMode.Studios, filterMode: GQL.FilterMode.Studios,
@@ -135,20 +135,12 @@ export const StudioList: React.FC<IStudioList> = ({
if (filter.displayMode === DisplayMode.Grid) { if (filter.displayMode === DisplayMode.Grid) {
return ( return (
<div className="row px-xl-5 justify-content-center"> <StudioCardGrid
{result.data.findStudios.studios.map((studio) => ( studios={result.data.findStudios.studios}
<StudioCard fromParent={fromParent}
key={studio.id} selectedIds={selectedIds}
studio={studio} onSelectChange={onSelectChange}
hideParent={fromParent} />
selecting={selectedIds.size > 0}
selected={selectedIds.has(studio.id)}
onSelectedChanged={(selected: boolean, shiftKey: boolean) =>
onSelectChange(studio.id, selected, shiftKey)
}
/>
))}
</div>
); );
} }
if (filter.displayMode === DisplayMode.List) { if (filter.displayMode === DisplayMode.List) {

View File

@@ -1,15 +1,17 @@
import { ButtonGroup } from "react-bootstrap"; import { ButtonGroup } from "react-bootstrap";
import React from "react"; import React, { useEffect, useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import NavUtils from "src/utils/navigation"; import NavUtils from "src/utils/navigation";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { TruncatedText } from "../Shared/TruncatedText"; import { TruncatedText } from "../Shared/TruncatedText";
import { GridCard } from "../Shared/GridCard"; import { GridCard, calculateCardWidth } from "../Shared/GridCard";
import { PopoverCountButton } from "../Shared/PopoverCountButton"; import { PopoverCountButton } from "../Shared/PopoverCountButton";
import ScreenUtils from "src/utils/screen";
interface IProps { interface IProps {
tag: GQL.TagDataFragment; tag: GQL.TagDataFragment;
containerWidth?: number;
zoomIndex: number; zoomIndex: number;
selecting?: boolean; selecting?: boolean;
selected?: boolean; selected?: boolean;
@@ -18,11 +20,40 @@ interface IProps {
export const TagCard: React.FC<IProps> = ({ export const TagCard: React.FC<IProps> = ({
tag, tag,
containerWidth,
zoomIndex, zoomIndex,
selecting, selecting,
selected, selected,
onSelectedChanged, onSelectedChanged,
}) => { }) => {
const [cardWidth, setCardWidth] = useState<number>();
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() { function maybeRenderDescription() {
if (tag.description) { if (tag.description) {
return ( return (
@@ -181,6 +212,7 @@ export const TagCard: React.FC<IProps> = ({
<GridCard <GridCard
className={`tag-card zoom-${zoomIndex}`} className={`tag-card zoom-${zoomIndex}`}
url={`/tags/${tag.id}`} url={`/tags/${tag.id}`}
width={cardWidth}
title={tag.name ?? ""} title={tag.name ?? ""}
linkClassName="tag-card-header" linkClassName="tag-card-header"
image={ image={

View File

@@ -0,0 +1,38 @@
import React, { useRef } from "react";
import * as GQL from "src/core/generated-graphql";
import { useContainerDimensions } from "../Shared/GridCard";
import { TagCard } from "./TagCard";
interface ITagCardGrid {
tags: GQL.TagDataFragment[];
selectedIds: Set<string>;
zoomIndex: number;
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void;
}
export const TagCardGrid: React.FC<ITagCardGrid> = ({
tags,
selectedIds,
zoomIndex,
onSelectChange,
}) => {
const componentRef = useRef<HTMLDivElement>(null);
const { width } = useContainerDimensions(componentRef);
return (
<div className="row justify-content-center" ref={componentRef}>
{tags.map((tag) => (
<TagCard
key={tag.id}
containerWidth={width}
tag={tag}
zoomIndex={zoomIndex}
selecting={selectedIds.size > 0}
selected={selectedIds.has(tag.id)}
onSelectedChanged={(selected: boolean, shiftKey: boolean) =>
onSelectChange(tag.id, selected, shiftKey)
}
/>
))}
</div>
);
};

View File

@@ -24,10 +24,10 @@ import NavUtils from "src/utils/navigation";
import { Icon } from "../Shared/Icon"; import { Icon } from "../Shared/Icon";
import { ModalComponent } from "../Shared/Modal"; import { ModalComponent } from "../Shared/Modal";
import { DeleteEntityDialog } from "../Shared/DeleteEntityDialog"; import { DeleteEntityDialog } from "../Shared/DeleteEntityDialog";
import { TagCard } from "./TagCard";
import { ExportDialog } from "../Shared/ExportDialog"; import { ExportDialog } from "../Shared/ExportDialog";
import { tagRelationHook } from "../../core/tags"; import { tagRelationHook } from "../../core/tags";
import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; import { faTrashAlt } from "@fortawesome/free-solid-svg-icons";
import { TagCardGrid } from "./TagCardGrid";
interface ITagList { interface ITagList {
filterHook?: (filter: ListFilterModel) => ListFilterModel; filterHook?: (filter: ListFilterModel) => ListFilterModel;
@@ -188,20 +188,12 @@ export const TagList: React.FC<ITagList> = ({ filterHook, alterQuery }) => {
if (filter.displayMode === DisplayMode.Grid) { if (filter.displayMode === DisplayMode.Grid) {
return ( return (
<div className="row px-xl-5 justify-content-center"> <TagCardGrid
{result.data.findTags.tags.map((tag) => ( tags={result.data.findTags.tags}
<TagCard zoomIndex={filter.zoomIndex}
key={tag.id} selectedIds={selectedIds}
tag={tag} onSelectChange={onSelectChange}
zoomIndex={filter.zoomIndex} />
selecting={selectedIds.size > 0}
selected={selectedIds.has(tag.id)}
onSelectedChanged={(selected: boolean, shiftKey: boolean) =>
onSelectChange(tag.id, selected, shiftKey)
}
/>
))}
</div>
); );
} }
if (filter.displayMode === DisplayMode.List) { if (filter.displayMode === DisplayMode.List) {

View File

@@ -487,16 +487,6 @@ textarea.text-input {
} }
.zoom-0 { .zoom-0 {
width: 240px;
.scene-card-preview {
height: 135px;
}
.portrait {
height: 180px;
}
.gallery-card-image, .gallery-card-image,
.tag-card-image { .tag-card-image {
max-height: 180px; max-height: 180px;
@@ -506,14 +496,6 @@ textarea.text-input {
.zoom-1 { .zoom-1 {
width: 320px; width: 320px;
.scene-card-preview {
height: 180px;
}
.portrait {
height: 240px;
}
.gallery-card-image, .gallery-card-image,
.tag-card-image { .tag-card-image {
max-height: 240px; max-height: 240px;
@@ -525,16 +507,6 @@ textarea.text-input {
} }
.zoom-2 { .zoom-2 {
width: 480px;
.scene-card-preview {
height: 270px;
}
.portrait {
height: 360px;
}
.gallery-card-image, .gallery-card-image,
.tag-card-image { .tag-card-image {
max-height: 360px; max-height: 360px;
@@ -546,16 +518,6 @@ textarea.text-input {
} }
.zoom-3 { .zoom-3 {
width: 640px;
.scene-card-preview {
height: 360px;
}
.portrait {
height: 480px;
}
.tag-card-image, .tag-card-image,
.gallery-card-image { .gallery-card-image {
max-height: 480px; max-height: 480px;

View File

@@ -1,5 +1,5 @@
const isMobile = () => const isMobile = () =>
window.matchMedia("only screen and (max-width: 767px)").matches; window.matchMedia("only screen and (max-width: 576px)").matches;
const ScreenUtils = { const ScreenUtils = {
isMobile, isMobile,