Fix card click selection (#1476)

* Make other cards use generic card
This commit is contained in:
WithoutPants
2021-06-04 12:11:17 +10:00
committed by GitHub
parent 2469012008
commit 732cc57149
13 changed files with 106 additions and 262 deletions

View File

@@ -8,7 +8,7 @@
* Added [DLNA server](/settings?tab=dlna). ([#1364](https://github.com/stashapp/stash/pull/1364))
### 🎨 Improvements
* Prompt when leaving scene edit page with unsaved changed. ([#1429](https://github.com/stashapp/stash/pull/1429))
* Prompt when leaving scene edit page with unsaved changes. ([#1429](https://github.com/stashapp/stash/pull/1429))
* Make multi-set mode buttons more obvious in multi-edit dialog. ([#1435](https://github.com/stashapp/stash/pull/1435))
* Filter modifiers and sort by options are now sorted alphabetically. ([#1406](https://github.com/stashapp/stash/pull/1406))
* Add `CreatedAt` and `UpdatedAt` (and `FileModTime` where applicable) to API objects. ([#1421](https://github.com/stashapp/stash/pull/1421))
@@ -18,6 +18,7 @@
* Add button to remove studio stash ID. ([#1378](https://github.com/stashapp/stash/pull/1378))
### 🐛 Bug fixes
* Fix click/drag to select scenes. ([#1476](https://github.com/stashapp/stash/pull/1476))
* Fix clearing Performer and Movie ratings not working. ([#1429](https://github.com/stashapp/stash/pull/1429))
* Fix scraper date parser failing when parsing time. ([#1431](https://github.com/stashapp/stash/pull/1431))
* Fix quotes in filter labels causing UI errors. ([#1425](https://github.com/stashapp/stash/pull/1425))

View File

@@ -4,13 +4,7 @@ import { Link } from "react-router-dom";
import * as GQL from "src/core/generated-graphql";
import { FormattedPlural } from "react-intl";
import { useConfiguration } from "src/core/StashService";
import {
BasicCard,
HoverPopover,
Icon,
TagLink,
TruncatedText,
} from "src/components/Shared";
import { GridCard, HoverPopover, Icon, TagLink } from "src/components/Shared";
import { TextUtils } from "src/utils";
import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton";
@@ -136,9 +130,14 @@ export const GalleryCard: React.FC<IProps> = (props) => {
}
return (
<BasicCard
<GridCard
className={`gallery-card zoom-${props.zoomIndex}`}
url={`/galleries/${props.gallery.id}`}
title={
props.gallery.title
? props.gallery.title
: TextUtils.fileNameFromPath(props.gallery.path ?? "")
}
linkClassName="gallery-card-header"
image={
<>
@@ -155,18 +154,6 @@ export const GalleryCard: React.FC<IProps> = (props) => {
overlays={maybeRenderSceneStudioOverlay()}
details={
<>
<Link to={`/galleries/${props.gallery.id}`}>
<h5 className="card-section-title">
<TruncatedText
text={
props.gallery.title
? props.gallery.title
: TextUtils.fileNameFromPath(props.gallery.path ?? "")
}
lineCount={2}
/>
</h5>
</Link>
<span>
{props.gallery.image_count}&nbsp;
<FormattedPlural

View File

@@ -1,17 +1,11 @@
import React from "react";
import { Button, ButtonGroup, Card, Form } from "react-bootstrap";
import { Link } from "react-router-dom";
import { Button, ButtonGroup } from "react-bootstrap";
import cx from "classnames";
import * as GQL from "src/core/generated-graphql";
import {
Icon,
TagLink,
HoverPopover,
SweatDrops,
TruncatedText,
} from "src/components/Shared";
import { Icon, TagLink, HoverPopover, SweatDrops } from "src/components/Shared";
import { TextUtils } from "src/utils";
import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton";
import { GridCard } from "../Shared/GridCard";
interface IImageCardProps {
image: GQL.SlimImageDataFragment;
@@ -110,36 +104,6 @@ export const ImageCard: React.FC<IImageCardProps> = (
}
}
function handleImageClick(
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
) {
const { shiftKey } = event;
if (props.selecting) {
props.onSelectedChanged(!props.selected, shiftKey);
event.preventDefault();
}
}
function handleDrag(event: React.DragEvent<HTMLAnchorElement>) {
if (props.selecting) {
event.dataTransfer.setData("text/plain", "");
event.dataTransfer.setDragImage(new Image(), 0, 0);
}
}
function handleDragOver(event: React.DragEvent<HTMLAnchorElement>) {
const ev = event;
const shiftKey = false;
if (props.selecting && !props.selected) {
props.onSelectedChanged(true, shiftKey);
}
ev.dataTransfer.dropEffect = "move";
ev.preventDefault();
}
function isPortrait() {
const { file } = props.image;
const width = file.width ? file.width : 0;
@@ -147,31 +111,18 @@ export const ImageCard: React.FC<IImageCardProps> = (
return height > width;
}
let shiftKey = false;
return (
<Card className={`image-card zoom-${props.zoomIndex}`}>
<Form.Control
type="checkbox"
className="image-card-check"
checked={props.selected}
onChange={() => props.onSelectedChanged(!props.selected, shiftKey)}
onClick={(event: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
// eslint-disable-next-line prefer-destructuring
shiftKey = event.shiftKey;
event.stopPropagation();
}}
/>
<div className="image-section">
<Link
to={`/images/${props.image.id}`}
className="image-card-link"
onClick={handleImageClick}
onDragStart={handleDrag}
onDragOver={handleDragOver}
draggable={props.selecting}
>
<GridCard
className={`image-card zoom-${props.zoomIndex}`}
url={`/images/${props.image.id}`}
title={
props.image.title
? props.image.title
: TextUtils.fileNameFromPath(props.image.path)
}
linkClassName="image-card-link"
image={
<>
<div className={cx("image-card-preview", { portrait: isPortrait() })}>
<img
className="image-card-preview-image"
@@ -180,22 +131,12 @@ export const ImageCard: React.FC<IImageCardProps> = (
/>
</div>
{maybeRenderRatingBanner()}
</Link>
</div>
<div className="card-section">
<h5 className="card-section-title">
<TruncatedText
text={
props.image.title
? props.image.title
: TextUtils.fileNameFromPath(props.image.path)
}
lineCount={2}
/>
</h5>
</div>
{maybeRenderPopoverButtonGroup()}
</Card>
</>
}
popovers={maybeRenderPopoverButtonGroup()}
selected={props.selected}
selecting={props.selecting}
onSelectedChanged={props.onSelectedChanged}
/>
);
};

View File

@@ -19,21 +19,6 @@
padding: 0;
}
&-check {
left: 0.5rem;
margin-top: -12px;
opacity: 0;
padding-left: 15px;
position: absolute;
top: 0.7rem;
width: 1.2rem;
z-index: 1;
&:checked {
opacity: 0.75;
}
}
.rating-banner {
transition: opacity 0.5s;
}
@@ -63,11 +48,6 @@
opacity: 0;
transition: opacity 0.5s;
}
.image-card-check {
opacity: 0.75;
transition: opacity 0.5s;
}
}
}

View File

@@ -1,7 +1,7 @@
import React, { FunctionComponent } from "react";
import { FormattedPlural } from "react-intl";
import * as GQL from "src/core/generated-graphql";
import { BasicCard, TruncatedText } from "src/components/Shared";
import { GridCard } from "src/components/Shared";
interface IProps {
movie: GQL.MovieDataFragment;
@@ -45,9 +45,10 @@ export const MovieCard: FunctionComponent<IProps> = (props: IProps) => {
}
return (
<BasicCard
<GridCard
className="movie-card"
url={`/movies/${props.movie.id}`}
title={props.movie.name}
linkClassName="movie-card-header"
image={
<>
@@ -59,14 +60,7 @@ export const MovieCard: FunctionComponent<IProps> = (props: IProps) => {
{maybeRenderRatingBanner()}
</>
}
details={
<>
<h5>
<TruncatedText text={props.movie.name} lineCount={2} />
</h5>
{maybeRenderSceneNumber()}
</>
}
details={maybeRenderSceneNumber()}
selected={props.selected}
selecting={props.selecting}
onSelectedChanged={props.onSelectedChanged}

View File

@@ -3,12 +3,11 @@ import { Link } from "react-router-dom";
import * as GQL from "src/core/generated-graphql";
import { NavUtils, TextUtils } from "src/utils";
import {
BasicCard,
GridCard,
CountryFlag,
HoverPopover,
Icon,
TagLink,
TruncatedText,
} from "src/components/Shared";
import { Button, ButtonGroup } from "react-bootstrap";
import {
@@ -150,9 +149,10 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
}
return (
<BasicCard
<GridCard
className="performer-card"
url={`/performers/${performer.id}`}
title={performer.name ?? ""}
image={
<>
<img
@@ -166,9 +166,6 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
}
details={
<>
<h5>
<TruncatedText text={performer.name} />
</h5>
{age !== 0 ? <div className="text-muted">{ageString}</div> : ""}
<Link to={NavUtils.makePerformersCountryUrl(performer)}>
<CountryFlag country={performer.country} />

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useRef } from "react";
import { Button, ButtonGroup, Card, Form } from "react-bootstrap";
import { Button, ButtonGroup } from "react-bootstrap";
import { Link } from "react-router-dom";
import cx from "classnames";
import * as GQL from "src/core/generated-graphql";
@@ -14,6 +14,7 @@ import {
import { TextUtils } from "src/utils";
import { SceneQueue } from "src/models/sceneQueue";
import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton";
import { GridCard } from "../Shared/GridCard";
interface IScenePreviewProps {
isPortrait: boolean;
@@ -294,36 +295,6 @@ export const SceneCard: React.FC<ISceneCardProps> = (
}
}
function handleSceneClick(
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
) {
const { shiftKey } = event;
if (props.selecting && props.onSelectedChanged) {
props.onSelectedChanged(!props.selected, shiftKey);
event.preventDefault();
}
}
function handleDrag(event: React.DragEvent<HTMLAnchorElement>) {
if (props.selecting) {
event.dataTransfer.setData("text/plain", "");
event.dataTransfer.setDragImage(new Image(), 0, 0);
}
}
function handleDragOver(event: React.DragEvent<HTMLAnchorElement>) {
const ev = event;
const shiftKey = false;
if (props.selecting && props.onSelectedChanged && !props.selected) {
props.onSelectedChanged(true, shiftKey);
}
ev.dataTransfer.dropEffect = "move";
ev.preventDefault();
}
function isPortrait() {
const { file } = props.scene;
const width = file.width ? file.width : 0;
@@ -337,35 +308,23 @@ export const SceneCard: React.FC<ISceneCardProps> = (
}
}
let shiftKey = false;
const sceneLink = props.queue
? props.queue.makeLink(props.scene.id, { sceneIndex: props.index })
: `/scenes/${props.scene.id}`;
return (
<Card className={`scene-card ${zoomIndex()}`}>
<Form.Control
type="checkbox"
className="scene-card-check"
checked={props.selected}
onChange={() => props.onSelectedChanged?.(!props.selected, shiftKey)}
onClick={(event: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
// eslint-disable-next-line prefer-destructuring
shiftKey = event.shiftKey;
event.stopPropagation();
}}
/>
<div className="video-section">
<Link
to={sceneLink}
className="scene-card-link"
onClick={handleSceneClick}
onDragStart={handleDrag}
onDragOver={handleDragOver}
draggable={props.selecting}
>
<GridCard
className={`scene-card ${zoomIndex()}`}
url={sceneLink}
title={
props.scene.title
? props.scene.title
: TextUtils.fileNameFromPath(props.scene.path)
}
linkClassName="scene-card-link"
thumbnailSectionClassName="video-section"
image={
<>
<ScenePreview
image={props.scene.paths.screenshot ?? undefined}
video={props.scene.paths.preview ?? undefined}
@@ -376,29 +335,21 @@ export const SceneCard: React.FC<ISceneCardProps> = (
/>
{maybeRenderRatingBanner()}
{maybeRenderSceneSpecsOverlay()}
</Link>
{maybeRenderSceneStudioOverlay()}
</div>
<div className="card-section">
<h5 className="card-section-title">
<Link to={`/scenes/${props.scene.id}`}>
<TruncatedText
text={
props.scene.title
? props.scene.title
: TextUtils.fileNameFromPath(props.scene.path)
}
lineCount={2}
/>
</Link>
</h5>
<span>{props.scene.date}</span>
<p>
<TruncatedText text={props.scene.details} lineCount={3} />
</p>
</div>
{maybeRenderPopoverButtonGroup()}
</Card>
</>
}
overlays={maybeRenderSceneStudioOverlay()}
details={
<>
<span>{props.scene.date}</span>
<p>
<TruncatedText text={props.scene.details} lineCount={3} />
</p>
</>
}
popovers={maybeRenderPopoverButtonGroup()}
selected={props.selected}
selecting={props.selecting}
onSelectedChanged={props.onSelectedChanged}
/>
);
};

View File

@@ -164,21 +164,6 @@ textarea.scene-description {
text-decoration: none;
}
&-check {
left: 0.5rem;
margin-top: -12px;
opacity: 0;
padding-left: 15px;
position: absolute;
top: 0.7rem;
width: 1.2rem;
z-index: 1;
&:checked {
opacity: 0.75;
}
}
.scene-specs-overlay,
.rating-banner,
.scene-studio-overlay {

View File

@@ -1,13 +1,16 @@
import React from "react";
import { Card, Form } from "react-bootstrap";
import { Link } from "react-router-dom";
import TruncatedText from "./TruncatedText";
interface IBasicCardProps {
interface ICardProps {
className?: string;
linkClassName?: string;
thumbnailSectionClassName?: string;
url: string;
title: string;
image: JSX.Element;
details: JSX.Element;
details?: JSX.Element;
overlays?: JSX.Element;
popovers?: JSX.Element;
selecting?: boolean;
@@ -15,12 +18,8 @@ interface IBasicCardProps {
onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void;
}
export const BasicCard: React.FC<IBasicCardProps> = (
props: IBasicCardProps
) => {
function handleImageClick(
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
) {
export const GridCard: React.FC<ICardProps> = (props: ICardProps) => {
function handleImageClick(event: React.MouseEvent<HTMLElement, MouseEvent>) {
const { shiftKey } = event;
if (!props.onSelectedChanged) {
@@ -33,14 +32,14 @@ export const BasicCard: React.FC<IBasicCardProps> = (
}
}
function handleDrag(event: React.DragEvent<HTMLAnchorElement>) {
function handleDrag(event: React.DragEvent<HTMLElement>) {
if (props.selecting) {
event.dataTransfer.setData("text/plain", "");
event.dataTransfer.setDragImage(new Image(), 0, 0);
}
}
function handleDragOver(event: React.DragEvent<HTMLAnchorElement>) {
function handleDragOver(event: React.DragEvent<HTMLElement>) {
const ev = event;
const shiftKey = false;
@@ -77,23 +76,33 @@ export const BasicCard: React.FC<IBasicCardProps> = (
}
return (
<Card className={props.className}>
<Card
className={`${props.className} grid-card`}
onClick={handleImageClick}
onDragStart={handleDrag}
onDragOver={handleDragOver}
draggable={props.onSelectedChanged && props.selecting}
>
{maybeRenderCheckbox()}
<div className="image-section">
<div className={`${props.thumbnailSectionClassName} thumbnail-section`}>
<Link
to={props.url}
className={props.linkClassName}
onClick={handleImageClick}
onDragStart={handleDrag}
onDragOver={handleDragOver}
draggable={props.onSelectedChanged && props.selecting}
>
{props.image}
</Link>
{props.overlays}
</div>
<div className="card-section">{props.details}</div>
<div className="card-section">
<Link to={props.url} onClick={handleImageClick}>
<h5 className="card-section-title">
<TruncatedText text={props.title} lineCount={2} />
</h5>
</Link>
{props.details}
</div>
{props.popovers}
</Card>

View File

@@ -13,7 +13,7 @@ export { default as CountryFlag } from "./CountryFlag";
export { default as SuccessIcon } from "./SuccessIcon";
export { default as ErrorMessage } from "./ErrorMessage";
export { default as TruncatedText } from "./TruncatedText";
export { BasicCard } from "./BasicCard";
export { GridCard } from "./GridCard";
export { RatingStars } from "./RatingStars";
export { ExportDialog } from "./ExportDialog";
export { default as DeleteEntityDialog } from "./DeleteEntityDialog";

View File

@@ -151,7 +151,12 @@ button.collapse-button.btn-primary:not(:disabled):not(.disabled):active {
}
}
.card {
.grid-card {
a .card-section-title {
color: $text-color;
text-decoration: none;
}
.card-check {
left: 0.5rem;
margin-top: -12px;

View File

@@ -2,7 +2,7 @@ import React from "react";
import { Link } from "react-router-dom";
import * as GQL from "src/core/generated-graphql";
import { NavUtils } from "src/utils";
import { BasicCard, TruncatedText } from "src/components/Shared";
import { GridCard } from "src/components/Shared";
import { ButtonGroup } from "react-bootstrap";
import { PopoverCountButton } from "../Shared/PopoverCountButton";
@@ -119,9 +119,10 @@ export const StudioCard: React.FC<IProps> = ({
}
return (
<BasicCard
<GridCard
className="studio-card"
url={`/studios/${studio.id}`}
title={studio.name}
linkClassName="studio-card-header"
image={
<img
@@ -132,9 +133,6 @@ export const StudioCard: React.FC<IProps> = ({
}
details={
<>
<h5>
<TruncatedText text={studio.name} />
</h5>
{maybeRenderParent(studio, hideParent)}
{maybeRenderChildren(studio)}
{maybeRenderRatingBanner(studio)}

View File

@@ -3,8 +3,8 @@ import React from "react";
import { Link } from "react-router-dom";
import * as GQL from "src/core/generated-graphql";
import { NavUtils } from "src/utils";
import { Icon, TruncatedText } from "../Shared";
import { BasicCard } from "../Shared/BasicCard";
import { Icon } from "../Shared";
import { GridCard } from "../Shared/GridCard";
import { PopoverCountButton } from "../Shared/PopoverCountButton";
interface IProps {
@@ -102,9 +102,10 @@ export const TagCard: React.FC<IProps> = ({
}
return (
<BasicCard
<GridCard
className={`tag-card zoom-${zoomIndex}`}
url={`/tags/${tag.id}`}
title={tag.name ?? ""}
linkClassName="tag-card-header"
image={
<img
@@ -113,11 +114,6 @@ export const TagCard: React.FC<IProps> = ({
src={tag.image_path ?? ""}
/>
}
details={
<h5>
<TruncatedText text={tag.name} />
</h5>
}
popovers={maybeRenderPopoverButtonGroup()}
selected={selected}
selecting={selecting}