mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Sort performers in popover and card views (#1294)
This commit is contained in:
@@ -38,6 +38,7 @@ fragment SlimImageData on Image {
|
|||||||
performers {
|
performers {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
gender
|
||||||
favorite
|
favorite
|
||||||
image_path
|
image_path
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ fragment SlimSceneData on Scene {
|
|||||||
performers {
|
performers {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
gender
|
||||||
favorite
|
favorite
|
||||||
image_path
|
image_path
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
* Added scene queue.
|
* Added scene queue.
|
||||||
|
|
||||||
### 🎨 Improvements
|
### 🎨 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 popover buttons for scenes/images/galleries on performer/studio/tag cards.
|
||||||
* Add slideshow to image wall view.
|
* Add slideshow to image wall view.
|
||||||
* Support API key via URL query parameter, and added API key to stream link in Scene File Info.
|
* Support API key via URL query parameter, and added API key to stream link in Scene File Info.
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
TruncatedText,
|
TruncatedText,
|
||||||
} from "src/components/Shared";
|
} from "src/components/Shared";
|
||||||
import { TextUtils } from "src/utils";
|
import { TextUtils } from "src/utils";
|
||||||
|
import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
gallery: GQL.GallerySlimDataFragment;
|
gallery: GQL.GallerySlimDataFragment;
|
||||||
@@ -63,30 +64,7 @@ export const GalleryCard: React.FC<IProps> = (props) => {
|
|||||||
function maybeRenderPerformerPopoverButton() {
|
function maybeRenderPerformerPopoverButton() {
|
||||||
if (props.gallery.performers.length <= 0) return;
|
if (props.gallery.performers.length <= 0) return;
|
||||||
|
|
||||||
const popoverContent = props.gallery.performers.map((performer) => (
|
return <PerformerPopoverButton performers={props.gallery.performers} />;
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function maybeRenderSceneStudioOverlay() {
|
function maybeRenderSceneStudioOverlay() {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { TextUtils } from "src/utils";
|
|||||||
import { TagLink, TruncatedText } from "src/components/Shared";
|
import { TagLink, TruncatedText } from "src/components/Shared";
|
||||||
import { PerformerCard } from "src/components/Performers/PerformerCard";
|
import { PerformerCard } from "src/components/Performers/PerformerCard";
|
||||||
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
||||||
|
import { sortPerformers } from "src/core/performers";
|
||||||
|
|
||||||
interface IGalleryDetailProps {
|
interface IGalleryDetailProps {
|
||||||
gallery: Partial<GQL.GalleryDataFragment>;
|
gallery: Partial<GQL.GalleryDataFragment>;
|
||||||
@@ -38,7 +39,8 @@ export const GalleryDetailPanel: React.FC<IGalleryDetailProps> = (props) => {
|
|||||||
function renderPerformers() {
|
function renderPerformers() {
|
||||||
if (!props.gallery.performers || props.gallery.performers.length === 0)
|
if (!props.gallery.performers || props.gallery.performers.length === 0)
|
||||||
return;
|
return;
|
||||||
const cards = props.gallery.performers.map((performer) => (
|
const performers = sortPerformers(props.gallery.performers);
|
||||||
|
const cards = performers.map((performer) => (
|
||||||
<PerformerCard
|
<PerformerCard
|
||||||
key={performer.id}
|
key={performer.id}
|
||||||
performer={performer}
|
performer={performer}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
TruncatedText,
|
TruncatedText,
|
||||||
} from "src/components/Shared";
|
} from "src/components/Shared";
|
||||||
import { TextUtils } from "src/utils";
|
import { TextUtils } from "src/utils";
|
||||||
|
import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton";
|
||||||
|
|
||||||
interface IImageCardProps {
|
interface IImageCardProps {
|
||||||
image: GQL.SlimImageDataFragment;
|
image: GQL.SlimImageDataFragment;
|
||||||
@@ -58,30 +59,7 @@ export const ImageCard: React.FC<IImageCardProps> = (
|
|||||||
function maybeRenderPerformerPopoverButton() {
|
function maybeRenderPerformerPopoverButton() {
|
||||||
if (props.image.performers.length <= 0) return;
|
if (props.image.performers.length <= 0) return;
|
||||||
|
|
||||||
const popoverContent = props.image.performers.map((performer) => (
|
return <PerformerPopoverButton performers={props.image.performers} />;
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function maybeRenderOCounter() {
|
function maybeRenderOCounter() {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { TextUtils } from "src/utils";
|
|||||||
import { TagLink, TruncatedText } from "src/components/Shared";
|
import { TagLink, TruncatedText } from "src/components/Shared";
|
||||||
import { PerformerCard } from "src/components/Performers/PerformerCard";
|
import { PerformerCard } from "src/components/Performers/PerformerCard";
|
||||||
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
||||||
|
import { sortPerformers } from "src/core/performers";
|
||||||
|
|
||||||
interface IImageDetailProps {
|
interface IImageDetailProps {
|
||||||
image: GQL.ImageDataFragment;
|
image: GQL.ImageDataFragment;
|
||||||
@@ -26,7 +27,8 @@ export const ImageDetailPanel: React.FC<IImageDetailProps> = (props) => {
|
|||||||
|
|
||||||
function renderPerformers() {
|
function renderPerformers() {
|
||||||
if (props.image.performers.length === 0) return;
|
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} />
|
<PerformerCard key={performer.id} performer={performer} />
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
TruncatedText,
|
TruncatedText,
|
||||||
} from "src/components/Shared";
|
} from "src/components/Shared";
|
||||||
import { TextUtils } from "src/utils";
|
import { TextUtils } from "src/utils";
|
||||||
|
import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton";
|
||||||
|
|
||||||
interface IScenePreviewProps {
|
interface IScenePreviewProps {
|
||||||
isPortrait: boolean;
|
isPortrait: boolean;
|
||||||
@@ -161,30 +162,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
|||||||
function maybeRenderPerformerPopoverButton() {
|
function maybeRenderPerformerPopoverButton() {
|
||||||
if (props.scene.performers.length <= 0) return;
|
if (props.scene.performers.length <= 0) return;
|
||||||
|
|
||||||
const popoverContent = props.scene.performers.map((performer) => (
|
return <PerformerPopoverButton performers={props.scene.performers} />;
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function maybeRenderMoviePopoverButton() {
|
function maybeRenderMoviePopoverButton() {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import * as GQL from "src/core/generated-graphql";
|
|||||||
import { TextUtils } from "src/utils";
|
import { TextUtils } from "src/utils";
|
||||||
import { TagLink, TruncatedText } from "src/components/Shared";
|
import { TagLink, TruncatedText } from "src/components/Shared";
|
||||||
import { PerformerCard } from "src/components/Performers/PerformerCard";
|
import { PerformerCard } from "src/components/Performers/PerformerCard";
|
||||||
|
import { sortPerformers } from "src/core/performers";
|
||||||
import { RatingStars } from "./RatingStars";
|
import { RatingStars } from "./RatingStars";
|
||||||
|
|
||||||
interface ISceneDetailProps {
|
interface ISceneDetailProps {
|
||||||
@@ -37,7 +38,8 @@ export const SceneDetailPanel: React.FC<ISceneDetailProps> = (props) => {
|
|||||||
|
|
||||||
function renderPerformers() {
|
function renderPerformers() {
|
||||||
if (props.scene.performers.length === 0) return;
|
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
|
<PerformerCard
|
||||||
key={performer.id}
|
key={performer.id}
|
||||||
performer={performer}
|
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;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user