mirror of
https://github.com/stashapp/stash.git
synced 2025-12-09 18:04:33 +01:00
Fit cards properly within their containers (#4514)
* created missing cards grids
This commit is contained in:
parent
330581283a
commit
a8df95c3a4
25 changed files with 460 additions and 124 deletions
|
|
@ -319,10 +319,6 @@
|
|||
.slick-list .performer-card.card {
|
||||
width: 16rem;
|
||||
}
|
||||
|
||||
.slick-list .performer-card-image {
|
||||
height: 24rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Icons */
|
||||
|
|
|
|||
|
|
@ -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<IProps> = (props) => {
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
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() {
|
||||
if (props.gallery.scenes.length === 0) return;
|
||||
|
|
@ -153,6 +186,7 @@ export const GalleryCard: React.FC<IProps> = (props) => {
|
|||
<GridCard
|
||||
className={`gallery-card zoom-${props.zoomIndex}`}
|
||||
url={`/galleries/${props.gallery.id}`}
|
||||
width={cardWidth}
|
||||
title={galleryTitle(props.gallery)}
|
||||
linkClassName="gallery-card-header"
|
||||
image={
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from "react";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import cloneDeep from "lodash-es/cloneDeep";
|
||||
import { useHistory } from "react-router-dom";
|
||||
|
|
@ -18,6 +18,7 @@ import { EditGalleriesDialog } from "./EditGalleriesDialog";
|
|||
import { DeleteGalleriesDialog } from "./DeleteGalleriesDialog";
|
||||
import { ExportDialog } from "../Shared/ExportDialog";
|
||||
import { GalleryListTable } from "./GalleryListTable";
|
||||
import { useContainerDimensions } from "../Shared/GridCard";
|
||||
|
||||
const GalleryItemList = makeItemList({
|
||||
filterMode: GQL.FilterMode.Galleries,
|
||||
|
|
@ -106,6 +107,9 @@ export const GalleryList: React.FC<IGalleryList> = ({
|
|||
setIsExportDialogOpen(true);
|
||||
}
|
||||
|
||||
const componentRef = useRef<HTMLDivElement>(null);
|
||||
const { width } = useContainerDimensions(componentRef);
|
||||
|
||||
function renderContent(
|
||||
result: GQL.FindGalleriesQueryResult,
|
||||
filter: ListFilterModel,
|
||||
|
|
@ -133,10 +137,11 @@ export const GalleryList: React.FC<IGalleryList> = ({
|
|||
|
||||
if (filter.displayMode === DisplayMode.Grid) {
|
||||
return (
|
||||
<div className="row justify-content-center">
|
||||
<div className="row justify-content-center" ref={componentRef}>
|
||||
{result.data.findGalleries.galleries.map((gallery) => (
|
||||
<GalleryCard
|
||||
key={gallery.id}
|
||||
containerWidth={width}
|
||||
gallery={gallery}
|
||||
zoomIndex={filter.zoomIndex}
|
||||
selecting={selectedIds.size > 0}
|
||||
|
|
|
|||
|
|
@ -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<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(
|
||||
() =>
|
||||
props.image.visual_files.length > 0
|
||||
|
|
@ -153,6 +187,7 @@ export const ImageCard: React.FC<IImageCardProps> = (
|
|||
<GridCard
|
||||
className={`image-card zoom-${props.zoomIndex}`}
|
||||
url={`/images/${props.image.id}`}
|
||||
width={cardWidth}
|
||||
title={objectTitle(props.image)}
|
||||
linkClassName="image-card-link"
|
||||
image={
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import React, {
|
|||
useMemo,
|
||||
MouseEvent,
|
||||
useContext,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { FormattedNumber, useIntl } from "react-intl";
|
||||
import cloneDeep from "lodash-es/cloneDeep";
|
||||
|
|
@ -32,6 +33,7 @@ import { objectTitle } from "src/core/files";
|
|||
import TextUtils from "src/utils/text";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { IUIConfig } from "src/core/config";
|
||||
import { useContainerDimensions } from "../Shared/GridCard";
|
||||
|
||||
interface IImageWallProps {
|
||||
images: GQL.SlimImageDataFragment[];
|
||||
|
|
@ -196,6 +198,9 @@ const ImageListImages: React.FC<IImageListImages> = ({
|
|||
ev.preventDefault();
|
||||
}
|
||||
|
||||
const componentRef = useRef<HTMLDivElement>(null);
|
||||
const { width } = useContainerDimensions(componentRef);
|
||||
|
||||
function renderImageCard(
|
||||
index: number,
|
||||
image: GQL.SlimImageDataFragment,
|
||||
|
|
@ -204,6 +209,7 @@ const ImageListImages: React.FC<IImageListImages> = ({
|
|||
return (
|
||||
<ImageCard
|
||||
key={image.id}
|
||||
containerWidth={width}
|
||||
image={image}
|
||||
zoomIndex={zoomIndex}
|
||||
selecting={selectedIds.size > 0}
|
||||
|
|
@ -220,7 +226,7 @@ const ImageListImages: React.FC<IImageListImages> = ({
|
|||
|
||||
if (filter.displayMode === DisplayMode.Grid) {
|
||||
return (
|
||||
<div className="row justify-content-center">
|
||||
<div className="row justify-content-center" ref={componentRef}>
|
||||
{images.map((image, index) =>
|
||||
renderImageCard(index, image, filter.zoomIndex)
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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<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() {
|
||||
if (!props.sceneIndex) return;
|
||||
|
||||
|
|
@ -71,6 +86,7 @@ export const MovieCard: React.FC<IProps> = (props: IProps) => {
|
|||
<GridCard
|
||||
className="movie-card"
|
||||
url={`/movies/${props.movie.id}`}
|
||||
width={cardWidth}
|
||||
title={props.movie.name}
|
||||
linkClassName="movie-card-header"
|
||||
image={
|
||||
|
|
|
|||
35
ui/v2.5/src/components/Movies/MovieCardGrid.tsx
Normal file
35
ui/v2.5/src/components/Movies/MovieCardGrid.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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<IMovieList> = ({ filterHook, alterQuery }) => {
|
|||
|
||||
if (filter.displayMode === DisplayMode.Grid) {
|
||||
return (
|
||||
<div className="row justify-content-center">
|
||||
{result.data.findMovies.movies.map((p) => (
|
||||
<MovieCard
|
||||
key={p.id}
|
||||
movie={p}
|
||||
selecting={selectedIds.size > 0}
|
||||
selected={selectedIds.has(p.id)}
|
||||
onSelectedChanged={(selected: boolean, shiftKey: boolean) =>
|
||||
onSelectChange(p.id, selected, shiftKey)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<MovieCardGrid
|
||||
movies={result.data.findMovies.movies}
|
||||
selectedIds={selectedIds}
|
||||
onSelectChange={onSelectChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (filter.displayMode === DisplayMode.List) {
|
||||
|
|
|
|||
|
|
@ -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<CriterionValue>[];
|
||||
|
|
@ -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<IPerformerCardProps> = ({
|
||||
performer,
|
||||
containerWidth,
|
||||
ageFromDate,
|
||||
selecting,
|
||||
selected,
|
||||
|
|
@ -66,6 +69,18 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
|||
);
|
||||
|
||||
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() {
|
||||
return (
|
||||
|
|
@ -251,6 +266,7 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
|||
<GridCard
|
||||
className="performer-card"
|
||||
url={`/performers/${performer.id}`}
|
||||
width={cardWidth}
|
||||
pretitleIcon={
|
||||
<GenderIcon className="gender-icon" gender={performer.gender} />
|
||||
}
|
||||
|
|
|
|||
39
ui/v2.5/src/components/Performers/PerformerCardGrid.tsx
Normal file
39
ui/v2.5/src/components/Performers/PerformerCardGrid.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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<IPerformerList> = ({
|
|||
|
||||
if (filter.displayMode === DisplayMode.Grid) {
|
||||
return (
|
||||
<div className="row justify-content-center">
|
||||
{result.data.findPerformers.performers.map((p) => (
|
||||
<PerformerCard
|
||||
key={p.id}
|
||||
performer={p}
|
||||
selecting={selectedIds.size > 0}
|
||||
selected={selectedIds.has(p.id)}
|
||||
onSelectedChanged={(selected: boolean, shiftKey: boolean) =>
|
||||
onSelectChange(p.id, selected, shiftKey)
|
||||
}
|
||||
extraCriteria={extraCriteria}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<PerformerCardGrid
|
||||
performers={result.data.findPerformers.performers}
|
||||
zoomIndex={filter.zoomIndex}
|
||||
selectedIds={selectedIds}
|
||||
onSelectChange={onSelectChange}
|
||||
extraCriteria={extraCriteria}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (filter.displayMode === DisplayMode.List) {
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@
|
|||
}
|
||||
|
||||
&-image {
|
||||
height: 30rem;
|
||||
aspect-ratio: 2/3;
|
||||
min-width: 11.25rem;
|
||||
object-fit: cover;
|
||||
object-position: top;
|
||||
|
|
|
|||
|
|
@ -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<IScenePlayerProps> = ({
|
|||
}
|
||||
|
||||
const onResize = () => {
|
||||
const show = window.innerHeight >= 450 && window.innerWidth >= 576;
|
||||
const show = window.innerHeight >= 450 && !ScreenUtils.isMobile();
|
||||
setShowScrubber(show);
|
||||
};
|
||||
onResize();
|
||||
|
|
|
|||
|
|
@ -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<IScenePreviewProps> = ({
|
|||
|
||||
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<number>();
|
||||
|
||||
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}
|
||||
|
|
|
|||
|
|
@ -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<ISceneCardsGrid> = ({
|
|||
zoomIndex,
|
||||
onSelectChange,
|
||||
}) => {
|
||||
const componentRef = useRef<HTMLDivElement>(null);
|
||||
const { width } = useContainerDimensions(componentRef);
|
||||
return (
|
||||
<div className="row justify-content-center">
|
||||
<div className="row justify-content-center" ref={componentRef}>
|
||||
{scenes.map((scene, index) => (
|
||||
<SceneCard
|
||||
key={scene.id}
|
||||
containerWidth={width}
|
||||
scene={scene}
|
||||
queue={queue}
|
||||
index={index}
|
||||
|
|
|
|||
|
|
@ -82,6 +82,10 @@ textarea.scene-description {
|
|||
}
|
||||
}
|
||||
|
||||
.justify-content-center .studio-card .studio-card-image {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.studio-card {
|
||||
padding: 0.5rem;
|
||||
|
||||
|
|
@ -164,6 +168,12 @@ textarea.scene-description {
|
|||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.scene-card {
|
||||
&-preview {
|
||||
aspect-ratio: 16/9;
|
||||
}
|
||||
}
|
||||
|
||||
.scene-card,
|
||||
.gallery-card {
|
||||
a {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Card, Form } from "react-bootstrap";
|
||||
import { Link } from "react-router-dom";
|
||||
import cx from "classnames";
|
||||
import { TruncatedText } from "./TruncatedText";
|
||||
import ScreenUtils from "src/utils/screen";
|
||||
|
||||
interface ICardProps {
|
||||
className?: string;
|
||||
linkClassName?: string;
|
||||
thumbnailSectionClassName?: string;
|
||||
width?: number;
|
||||
url: string;
|
||||
pretitleIcon?: JSX.Element;
|
||||
title: JSX.Element | string;
|
||||
|
|
@ -23,6 +25,46 @@ interface ICardProps {
|
|||
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) => {
|
||||
function handleImageClick(event: React.MouseEvent<HTMLElement, MouseEvent>) {
|
||||
const { shiftKey } = event;
|
||||
|
|
@ -116,6 +158,11 @@ export const GridCard: React.FC<ICardProps> = (props: ICardProps) => {
|
|||
onDragStart={handleDrag}
|
||||
onDragOver={handleDragOver}
|
||||
draggable={props.onSelectedChanged && props.selecting}
|
||||
style={
|
||||
props.width && !ScreenUtils.isMobile()
|
||||
? { width: `${props.width}px` }
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{maybeRenderCheckbox()}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<IProps> = ({
|
||||
studio,
|
||||
containerWidth,
|
||||
hideParent,
|
||||
selecting,
|
||||
selected,
|
||||
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() {
|
||||
if (!studio.scene_count) return;
|
||||
|
||||
|
|
@ -156,6 +172,7 @@ export const StudioCard: React.FC<IProps> = ({
|
|||
<GridCard
|
||||
className="studio-card"
|
||||
url={`/studios/${studio.id}`}
|
||||
width={cardWidth}
|
||||
title={studio.name}
|
||||
linkClassName="studio-card-header"
|
||||
image={
|
||||
|
|
|
|||
38
ui/v2.5/src/components/Studios/StudioCardGrid.tsx
Normal file
38
ui/v2.5/src/components/Studios/StudioCardGrid.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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<IStudioList> = ({
|
|||
|
||||
if (filter.displayMode === DisplayMode.Grid) {
|
||||
return (
|
||||
<div className="row px-xl-5 justify-content-center">
|
||||
{result.data.findStudios.studios.map((studio) => (
|
||||
<StudioCard
|
||||
key={studio.id}
|
||||
studio={studio}
|
||||
hideParent={fromParent}
|
||||
selecting={selectedIds.size > 0}
|
||||
selected={selectedIds.has(studio.id)}
|
||||
onSelectedChanged={(selected: boolean, shiftKey: boolean) =>
|
||||
onSelectChange(studio.id, selected, shiftKey)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<StudioCardGrid
|
||||
studios={result.data.findStudios.studios}
|
||||
fromParent={fromParent}
|
||||
selectedIds={selectedIds}
|
||||
onSelectChange={onSelectChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (filter.displayMode === DisplayMode.List) {
|
||||
|
|
|
|||
|
|
@ -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<IProps> = ({
|
||||
tag,
|
||||
containerWidth,
|
||||
zoomIndex,
|
||||
selecting,
|
||||
selected,
|
||||
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() {
|
||||
if (tag.description) {
|
||||
return (
|
||||
|
|
@ -181,6 +212,7 @@ export const TagCard: React.FC<IProps> = ({
|
|||
<GridCard
|
||||
className={`tag-card zoom-${zoomIndex}`}
|
||||
url={`/tags/${tag.id}`}
|
||||
width={cardWidth}
|
||||
title={tag.name ?? ""}
|
||||
linkClassName="tag-card-header"
|
||||
image={
|
||||
|
|
|
|||
38
ui/v2.5/src/components/Tags/TagCardGrid.tsx
Normal file
38
ui/v2.5/src/components/Tags/TagCardGrid.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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<ITagList> = ({ filterHook, alterQuery }) => {
|
|||
|
||||
if (filter.displayMode === DisplayMode.Grid) {
|
||||
return (
|
||||
<div className="row px-xl-5 justify-content-center">
|
||||
{result.data.findTags.tags.map((tag) => (
|
||||
<TagCard
|
||||
key={tag.id}
|
||||
tag={tag}
|
||||
zoomIndex={filter.zoomIndex}
|
||||
selecting={selectedIds.size > 0}
|
||||
selected={selectedIds.has(tag.id)}
|
||||
onSelectedChanged={(selected: boolean, shiftKey: boolean) =>
|
||||
onSelectChange(tag.id, selected, shiftKey)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<TagCardGrid
|
||||
tags={result.data.findTags.tags}
|
||||
zoomIndex={filter.zoomIndex}
|
||||
selectedIds={selectedIds}
|
||||
onSelectChange={onSelectChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (filter.displayMode === DisplayMode.List) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue