mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Rating stars (#567)
* Add ratings stars control. Add to scene details * Replace rating with stars on edit panel * Add changelog entry
This commit is contained in:
@@ -85,7 +85,6 @@
|
|||||||
"string-no-newline": true,
|
"string-no-newline": true,
|
||||||
"string-quotes": "double",
|
"string-quotes": "double",
|
||||||
"time-min-milliseconds": 100,
|
"time-min-milliseconds": 100,
|
||||||
"unit-blacklist": ["em"],
|
|
||||||
"value-list-comma-space-after": "always-single-line",
|
"value-list-comma-space-after": "always-single-line",
|
||||||
"value-list-comma-space-before": "never",
|
"value-list-comma-space-before": "never",
|
||||||
"value-no-vendor-prefix": true
|
"value-no-vendor-prefix": true
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
"@apollo/react-hooks": "^3.1.5",
|
"@apollo/react-hooks": "^3.1.5",
|
||||||
"@formatjs/intl-numberformat": "^4.2.1",
|
"@formatjs/intl-numberformat": "^4.2.1",
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.28",
|
"@fortawesome/fontawesome-svg-core": "^1.2.28",
|
||||||
|
"@fortawesome/free-regular-svg-icons": "^5.13.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.13.0",
|
"@fortawesome/free-solid-svg-icons": "^5.13.0",
|
||||||
"@fortawesome/react-fontawesome": "^0.1.9",
|
"@fortawesome/react-fontawesome": "^0.1.9",
|
||||||
"apollo-cache": "^1.3.4",
|
"apollo-cache": "^1.3.4",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import ReactMarkdown from "react-markdown";
|
|||||||
|
|
||||||
const markup = `
|
const markup = `
|
||||||
### 🎨 Improvements
|
### 🎨 Improvements
|
||||||
|
* Show rating as stars in scene page.
|
||||||
* Add reload scrapers button.
|
* Add reload scrapers button.
|
||||||
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
118
ui/v2.5/src/components/Scenes/SceneDetails/RatingStars.tsx
Normal file
118
ui/v2.5/src/components/Scenes/SceneDetails/RatingStars.tsx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { Button } from "react-bootstrap";
|
||||||
|
import { Icon } from "src/components/Shared";
|
||||||
|
|
||||||
|
export interface IRatingStarsProps {
|
||||||
|
value?: number;
|
||||||
|
onSetRating?: (value?: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RatingStars: React.FC<IRatingStarsProps> = (
|
||||||
|
props: IRatingStarsProps
|
||||||
|
) => {
|
||||||
|
const [hoverRating, setHoverRating] = useState<number | undefined>();
|
||||||
|
const disabled = !props.onSetRating;
|
||||||
|
|
||||||
|
function setRating(rating: number) {
|
||||||
|
if (!props.onSetRating) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newRating: number | undefined = rating;
|
||||||
|
|
||||||
|
// unset if we're clicking on the current rating
|
||||||
|
if (props.value === rating) {
|
||||||
|
newRating = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the hover rating to undefined so that it doesn't immediately clear
|
||||||
|
// the stars
|
||||||
|
setHoverRating(undefined);
|
||||||
|
|
||||||
|
props.onSetRating(newRating);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIconPrefix(rating: number) {
|
||||||
|
if (hoverRating && hoverRating >= rating) {
|
||||||
|
if (hoverRating === props.value) {
|
||||||
|
return "far";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "fas";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hoverRating && props.value && props.value >= rating) {
|
||||||
|
return "fas";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "far";
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseOver(rating: number) {
|
||||||
|
if (!disabled) {
|
||||||
|
setHoverRating(rating);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseOut(rating: number) {
|
||||||
|
if (!disabled && hoverRating === rating) {
|
||||||
|
setHoverRating(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getClassName(rating: number) {
|
||||||
|
if (hoverRating && hoverRating >= rating) {
|
||||||
|
if (hoverRating === props.value) {
|
||||||
|
return "unsetting";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "setting";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.value && props.value >= rating) {
|
||||||
|
return "set";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unset";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTooltip(rating: number) {
|
||||||
|
if (disabled && props.value) {
|
||||||
|
// always return current rating for disabled control
|
||||||
|
return props.value.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!disabled) {
|
||||||
|
return rating.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderRatingButton = (rating: number) => (
|
||||||
|
<Button
|
||||||
|
disabled={disabled}
|
||||||
|
className="minimal"
|
||||||
|
onClick={() => setRating(rating)}
|
||||||
|
variant="secondary"
|
||||||
|
onMouseOver={() => onMouseOver(rating)}
|
||||||
|
onMouseOut={() => onMouseOut(rating)}
|
||||||
|
onFocus={() => onMouseOver(rating)}
|
||||||
|
onBlur={() => onMouseOut(rating)}
|
||||||
|
title={getTooltip(rating)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={[getIconPrefix(rating), "star"]}
|
||||||
|
className={getClassName(rating)}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
const maxRating = 5;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rating-stars">
|
||||||
|
{Array.from(Array(maxRating)).map((value, index) =>
|
||||||
|
renderRatingButton(index + 1)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -4,6 +4,7 @@ import { FormattedDate } from "react-intl";
|
|||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { TextUtils } from "src/utils";
|
import { TextUtils } from "src/utils";
|
||||||
import { TagLink } from "src/components/Shared";
|
import { TagLink } from "src/components/Shared";
|
||||||
|
import { RatingStars } from "./RatingStars";
|
||||||
|
|
||||||
interface ISceneDetailProps {
|
interface ISceneDetailProps {
|
||||||
scene: GQL.SceneDataFragment;
|
scene: GQL.SceneDataFragment;
|
||||||
@@ -44,7 +45,13 @@ export const SceneDetailPanel: React.FC<ISceneDetailProps> = (props) => {
|
|||||||
<FormattedDate value={props.scene.date} format="long" />
|
<FormattedDate value={props.scene.date} format="long" />
|
||||||
</h4>
|
</h4>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
{props.scene.rating ? <h6>Rating: {props.scene.rating}</h6> : ""}
|
{props.scene.rating ? (
|
||||||
|
<h6>
|
||||||
|
Rating: <RatingStars value={props.scene.rating} />
|
||||||
|
</h6>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
{props.scene.file.height && (
|
{props.scene.file.height && (
|
||||||
<h6>Resolution: {TextUtils.resolution(props.scene.file.height)}</h6>
|
<h6>Resolution: {TextUtils.resolution(props.scene.file.height)}</h6>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { useToast } from "src/hooks";
|
|||||||
import { ImageUtils, TableUtils } from "src/utils";
|
import { ImageUtils, TableUtils } from "src/utils";
|
||||||
import { MovieSelect } from "src/components/Shared/Select";
|
import { MovieSelect } from "src/components/Shared/Select";
|
||||||
import { SceneMovieTable, MovieSceneIndexMap } from "./SceneMovieTable";
|
import { SceneMovieTable, MovieSceneIndexMap } from "./SceneMovieTable";
|
||||||
|
import { RatingStars } from "./RatingStars";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
scene: GQL.SceneDataFragment;
|
scene: GQL.SceneDataFragment;
|
||||||
@@ -441,14 +442,15 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
|||||||
onChange: setDate,
|
onChange: setDate,
|
||||||
placeholder: "YYYY-MM-DD",
|
placeholder: "YYYY-MM-DD",
|
||||||
})}
|
})}
|
||||||
{TableUtils.renderHtmlSelect({
|
<tr className="rating">
|
||||||
title: "Rating",
|
<td>Rating</td>
|
||||||
value: rating,
|
<td>
|
||||||
isEditing: true,
|
<RatingStars
|
||||||
onChange: (value: string) =>
|
value={rating}
|
||||||
setRating(Number.parseInt(value, 10)),
|
onSetRating={(value) => setRating(value)}
|
||||||
selectOptions: ["", 1, 2, 3, 4, 5],
|
/>
|
||||||
})}
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Gallery</td>
|
<td>Gallery</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -218,3 +218,44 @@
|
|||||||
.movie-table td {
|
.movie-table td {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rating-stars {
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-size: inherit;
|
||||||
|
margin-right: 1px;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background-color: inherit;
|
||||||
|
opacity: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.unsetting {
|
||||||
|
color: gold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting {
|
||||||
|
color: gold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.set {
|
||||||
|
color: gold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating td {
|
||||||
|
padding-bottom: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#scene-edit-details .rating-stars {
|
||||||
|
font-size: 1.3em;
|
||||||
|
height: calc(1.5em + 0.75rem + 2px);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { IconName } from "@fortawesome/fontawesome-svg-core";
|
import { IconProp, library } from "@fortawesome/fontawesome-svg-core";
|
||||||
|
import { faStar as fasStar } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { faStar as farStar } from "@fortawesome/free-regular-svg-icons";
|
||||||
|
|
||||||
|
// need these to use far and fas styles of stars
|
||||||
|
library.add(fasStar, farStar);
|
||||||
|
|
||||||
interface IIcon {
|
interface IIcon {
|
||||||
icon: IconName;
|
icon: IconProp;
|
||||||
className?: string;
|
className?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1761,6 +1761,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@fortawesome/fontawesome-common-types" "^0.2.28"
|
"@fortawesome/fontawesome-common-types" "^0.2.28"
|
||||||
|
|
||||||
|
"@fortawesome/free-regular-svg-icons@^5.13.0":
|
||||||
|
version "5.13.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.13.0.tgz#925a13d8bdda0678f71551828cac80ab47b8150c"
|
||||||
|
integrity sha512-70FAyiS5j+ANYD4dh9NGowTorNDnyvQHHpCM7FpnF7GxtDjBUCKdrFqCPzesEIpNDFNd+La3vex+jDk4nnUfpA==
|
||||||
|
dependencies:
|
||||||
|
"@fortawesome/fontawesome-common-types" "^0.2.28"
|
||||||
|
|
||||||
"@fortawesome/free-solid-svg-icons@^5.13.0":
|
"@fortawesome/free-solid-svg-icons@^5.13.0":
|
||||||
version "5.13.0"
|
version "5.13.0"
|
||||||
resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.13.0.tgz#44d9118668ad96b4fd5c9434a43efc5903525739"
|
resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.13.0.tgz#44d9118668ad96b4fd5c9434a43efc5903525739"
|
||||||
|
|||||||
Reference in New Issue
Block a user