mirror of
https://github.com/stashapp/stash.git
synced 2025-12-16 05:13:46 +01:00
Sort performers in popover and card views (#1294)
This commit is contained in:
parent
e59018acfb
commit
0b40017b09
11 changed files with 93 additions and 75 deletions
|
|
@ -38,6 +38,7 @@ fragment SlimImageData on Image {
|
|||
performers {
|
||||
id
|
||||
name
|
||||
gender
|
||||
favorite
|
||||
image_path
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ fragment SlimSceneData on Scene {
|
|||
performers {
|
||||
id
|
||||
name
|
||||
gender
|
||||
favorite
|
||||
image_path
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
* Added scene queue.
|
||||
|
||||
### 🎨 Improvements
|
||||
* Sort performers by gender in scene/image/gallery cards and details.
|
||||
* Add popover buttons for scenes/images/galleries on performer/studio/tag cards.
|
||||
* Add slideshow to image wall view.
|
||||
* Support API key via URL query parameter, and added API key to stream link in Scene File Info.
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
TruncatedText,
|
||||
} from "src/components/Shared";
|
||||
import { TextUtils } from "src/utils";
|
||||
import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton";
|
||||
|
||||
interface IProps {
|
||||
gallery: GQL.GallerySlimDataFragment;
|
||||
|
|
@ -63,30 +64,7 @@ export const GalleryCard: React.FC<IProps> = (props) => {
|
|||
function maybeRenderPerformerPopoverButton() {
|
||||
if (props.gallery.performers.length <= 0) return;
|
||||
|
||||
const popoverContent = props.gallery.performers.map((performer) => (
|
||||
<div className="performer-tag-container row" key={performer.id}>
|
||||
<Link
|
||||
to={`/performers/${performer.id}`}
|
||||
className="performer-tag col m-auto zoom-2"
|
||||
>
|
||||
<img
|
||||
className="image-thumbnail"
|
||||
alt={performer.name ?? ""}
|
||||
src={performer.image_path ?? ""}
|
||||
/>
|
||||
</Link>
|
||||
<TagLink key={performer.id} performer={performer} className="d-block" />
|
||||
</div>
|
||||
));
|
||||
|
||||
return (
|
||||
<HoverPopover placement="bottom" content={popoverContent}>
|
||||
<Button className="minimal">
|
||||
<Icon icon="user" />
|
||||
<span>{props.gallery.performers.length}</span>
|
||||
</Button>
|
||||
</HoverPopover>
|
||||
);
|
||||
return <PerformerPopoverButton performers={props.gallery.performers} />;
|
||||
}
|
||||
|
||||
function maybeRenderSceneStudioOverlay() {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { TextUtils } from "src/utils";
|
|||
import { TagLink, TruncatedText } from "src/components/Shared";
|
||||
import { PerformerCard } from "src/components/Performers/PerformerCard";
|
||||
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
||||
import { sortPerformers } from "src/core/performers";
|
||||
|
||||
interface IGalleryDetailProps {
|
||||
gallery: Partial<GQL.GalleryDataFragment>;
|
||||
|
|
@ -38,7 +39,8 @@ export const GalleryDetailPanel: React.FC<IGalleryDetailProps> = (props) => {
|
|||
function renderPerformers() {
|
||||
if (!props.gallery.performers || props.gallery.performers.length === 0)
|
||||
return;
|
||||
const cards = props.gallery.performers.map((performer) => (
|
||||
const performers = sortPerformers(props.gallery.performers);
|
||||
const cards = performers.map((performer) => (
|
||||
<PerformerCard
|
||||
key={performer.id}
|
||||
performer={performer}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
TruncatedText,
|
||||
} from "src/components/Shared";
|
||||
import { TextUtils } from "src/utils";
|
||||
import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton";
|
||||
|
||||
interface IImageCardProps {
|
||||
image: GQL.SlimImageDataFragment;
|
||||
|
|
@ -58,30 +59,7 @@ export const ImageCard: React.FC<IImageCardProps> = (
|
|||
function maybeRenderPerformerPopoverButton() {
|
||||
if (props.image.performers.length <= 0) return;
|
||||
|
||||
const popoverContent = props.image.performers.map((performer) => (
|
||||
<div className="performer-tag-container row" key={performer.id}>
|
||||
<Link
|
||||
to={`/performers/${performer.id}`}
|
||||
className="performer-tag col m-auto zoom-2"
|
||||
>
|
||||
<img
|
||||
className="image-thumbnail"
|
||||
alt={performer.name ?? ""}
|
||||
src={performer.image_path ?? ""}
|
||||
/>
|
||||
</Link>
|
||||
<TagLink key={performer.id} performer={performer} className="d-block" />
|
||||
</div>
|
||||
));
|
||||
|
||||
return (
|
||||
<HoverPopover placement="bottom" content={popoverContent}>
|
||||
<Button className="minimal">
|
||||
<Icon icon="user" />
|
||||
<span>{props.image.performers.length}</span>
|
||||
</Button>
|
||||
</HoverPopover>
|
||||
);
|
||||
return <PerformerPopoverButton performers={props.image.performers} />;
|
||||
}
|
||||
|
||||
function maybeRenderOCounter() {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { TextUtils } from "src/utils";
|
|||
import { TagLink, TruncatedText } from "src/components/Shared";
|
||||
import { PerformerCard } from "src/components/Performers/PerformerCard";
|
||||
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
||||
import { sortPerformers } from "src/core/performers";
|
||||
|
||||
interface IImageDetailProps {
|
||||
image: GQL.ImageDataFragment;
|
||||
|
|
@ -26,7 +27,8 @@ export const ImageDetailPanel: React.FC<IImageDetailProps> = (props) => {
|
|||
|
||||
function renderPerformers() {
|
||||
if (props.image.performers.length === 0) return;
|
||||
const cards = props.image.performers.map((performer) => (
|
||||
const performers = sortPerformers(props.image.performers);
|
||||
const cards = performers.map((performer) => (
|
||||
<PerformerCard key={performer.id} performer={performer} />
|
||||
));
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
TruncatedText,
|
||||
} from "src/components/Shared";
|
||||
import { TextUtils } from "src/utils";
|
||||
import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton";
|
||||
|
||||
interface IScenePreviewProps {
|
||||
isPortrait: boolean;
|
||||
|
|
@ -161,30 +162,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
|||
function maybeRenderPerformerPopoverButton() {
|
||||
if (props.scene.performers.length <= 0) return;
|
||||
|
||||
const popoverContent = props.scene.performers.map((performer) => (
|
||||
<div className="performer-tag-container row" key={performer.id}>
|
||||
<Link
|
||||
to={`/performers/${performer.id}`}
|
||||
className="performer-tag col m-auto zoom-2"
|
||||
>
|
||||
<img
|
||||
className="image-thumbnail"
|
||||
alt={performer.name ?? ""}
|
||||
src={performer.image_path ?? ""}
|
||||
/>
|
||||
</Link>
|
||||
<TagLink key={performer.id} performer={performer} className="d-block" />
|
||||
</div>
|
||||
));
|
||||
|
||||
return (
|
||||
<HoverPopover placement="bottom" content={popoverContent}>
|
||||
<Button className="minimal">
|
||||
<Icon icon="user" />
|
||||
<span>{props.scene.performers.length}</span>
|
||||
</Button>
|
||||
</HoverPopover>
|
||||
);
|
||||
return <PerformerPopoverButton performers={props.scene.performers} />;
|
||||
}
|
||||
|
||||
function maybeRenderMoviePopoverButton() {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import * as GQL from "src/core/generated-graphql";
|
|||
import { TextUtils } from "src/utils";
|
||||
import { TagLink, TruncatedText } from "src/components/Shared";
|
||||
import { PerformerCard } from "src/components/Performers/PerformerCard";
|
||||
import { sortPerformers } from "src/core/performers";
|
||||
import { RatingStars } from "./RatingStars";
|
||||
|
||||
interface ISceneDetailProps {
|
||||
|
|
@ -37,7 +38,8 @@ export const SceneDetailPanel: React.FC<ISceneDetailProps> = (props) => {
|
|||
|
||||
function renderPerformers() {
|
||||
if (props.scene.performers.length === 0) return;
|
||||
const cards = props.scene.performers.map((performer) => (
|
||||
const performers = sortPerformers(props.scene.performers);
|
||||
const cards = performers.map((performer) => (
|
||||
<PerformerCard
|
||||
key={performer.id}
|
||||
performer={performer}
|
||||
|
|
|
|||
40
ui/v2.5/src/components/Shared/PerformerPopoverButton.tsx
Normal file
40
ui/v2.5/src/components/Shared/PerformerPopoverButton.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import React from "react";
|
||||
import { Button } from "react-bootstrap";
|
||||
import { Link } from "react-router-dom";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { sortPerformers } from "src/core/performers";
|
||||
import { HoverPopover } from "./HoverPopover";
|
||||
import Icon from "./Icon";
|
||||
import { TagLink } from "./TagLink";
|
||||
|
||||
interface IProps {
|
||||
performers: Partial<GQL.PerformerDataFragment>[];
|
||||
}
|
||||
|
||||
export const PerformerPopoverButton: React.FC<IProps> = ({ performers }) => {
|
||||
const sorted = sortPerformers(performers);
|
||||
const popoverContent = sorted.map((performer) => (
|
||||
<div className="performer-tag-container row" key={performer.id}>
|
||||
<Link
|
||||
to={`/performers/${performer.id}`}
|
||||
className="performer-tag col m-auto zoom-2"
|
||||
>
|
||||
<img
|
||||
className="image-thumbnail"
|
||||
alt={performer.name ?? ""}
|
||||
src={performer.image_path ?? ""}
|
||||
/>
|
||||
</Link>
|
||||
<TagLink key={performer.id} performer={performer} className="d-block" />
|
||||
</div>
|
||||
));
|
||||
|
||||
return (
|
||||
<HoverPopover placement="bottom" content={popoverContent}>
|
||||
<Button className="minimal">
|
||||
<Icon icon="user" />
|
||||
<span>{performers.length}</span>
|
||||
</Button>
|
||||
</HoverPopover>
|
||||
);
|
||||
};
|
||||
|
|
@ -37,3 +37,38 @@ export const performerFilterHook = (
|
|||
return filter;
|
||||
};
|
||||
};
|
||||
|
||||
interface IPerformerFragment {
|
||||
name?: GQL.Maybe<string>;
|
||||
gender?: GQL.Maybe<GQL.GenderEnum>;
|
||||
}
|
||||
|
||||
export function sortPerformers<T extends IPerformerFragment>(performers: T[]) {
|
||||
const ret = performers.slice();
|
||||
ret.sort((a, b) => {
|
||||
if (a.gender === b.gender) {
|
||||
// sort by name
|
||||
return (a.name ?? "").localeCompare(b.name ?? "");
|
||||
}
|
||||
|
||||
// TODO - may want to customise gender order
|
||||
const genderOrder = [
|
||||
GQL.GenderEnum.Female,
|
||||
GQL.GenderEnum.TransgenderFemale,
|
||||
GQL.GenderEnum.Male,
|
||||
GQL.GenderEnum.TransgenderMale,
|
||||
GQL.GenderEnum.Intersex,
|
||||
GQL.GenderEnum.NonBinary,
|
||||
];
|
||||
|
||||
const aIndex = a.gender
|
||||
? genderOrder.indexOf(a.gender)
|
||||
: genderOrder.length;
|
||||
const bIndex = b.gender
|
||||
? genderOrder.indexOf(b.gender)
|
||||
: genderOrder.length;
|
||||
return aIndex - bIndex;
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue