mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 04:14:39 +03:00
Rating system patched components (#5912)
This commit is contained in:
@@ -4,6 +4,7 @@ import { Icon } from "../Icon";
|
|||||||
import { faPencil, faStar } from "@fortawesome/free-solid-svg-icons";
|
import { faPencil, faStar } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { useFocusOnce } from "src/utils/focus";
|
import { useFocusOnce } from "src/utils/focus";
|
||||||
import { useStopWheelScroll } from "src/utils/form";
|
import { useStopWheelScroll } from "src/utils/form";
|
||||||
|
import { PatchComponent } from "src/patch";
|
||||||
|
|
||||||
export interface IRatingNumberProps {
|
export interface IRatingNumberProps {
|
||||||
value: number | null;
|
value: number | null;
|
||||||
@@ -14,151 +15,152 @@ export interface IRatingNumberProps {
|
|||||||
withoutContext?: boolean;
|
withoutContext?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RatingNumber: React.FC<IRatingNumberProps> = (
|
export const RatingNumber = PatchComponent(
|
||||||
props: IRatingNumberProps
|
"RatingNumber",
|
||||||
) => {
|
(props: IRatingNumberProps) => {
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [valueStage, setValueStage] = useState<number | null>(props.value);
|
const [valueStage, setValueStage] = useState<number | null>(props.value);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setValueStage(props.value);
|
setValueStage(props.value);
|
||||||
}, [props.value]);
|
}, [props.value]);
|
||||||
|
|
||||||
const showTextField = !props.disabled && (editing || !props.clickToRate);
|
const showTextField = !props.disabled && (editing || !props.clickToRate);
|
||||||
|
|
||||||
const [ratingRef] = useFocusOnce(editing, true);
|
const [ratingRef] = useFocusOnce(editing, true);
|
||||||
useStopWheelScroll(ratingRef);
|
useStopWheelScroll(ratingRef);
|
||||||
|
|
||||||
const effectiveValue = editing ? valueStage : props.value;
|
const effectiveValue = editing ? valueStage : props.value;
|
||||||
|
|
||||||
const text = ((effectiveValue ?? 0) / 10).toFixed(1);
|
const text = ((effectiveValue ?? 0) / 10).toFixed(1);
|
||||||
const useValidation = useRef(true);
|
const useValidation = useRef(true);
|
||||||
|
|
||||||
function stepChange() {
|
function stepChange() {
|
||||||
useValidation.current = false;
|
useValidation.current = false;
|
||||||
}
|
|
||||||
|
|
||||||
function nonStepChange() {
|
|
||||||
useValidation.current = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setCursorPosition(
|
|
||||||
target: HTMLInputElement,
|
|
||||||
pos: number,
|
|
||||||
endPos?: number
|
|
||||||
) {
|
|
||||||
// This is a workaround to a missing feature where you can't set cursor position in input numbers.
|
|
||||||
// See https://stackoverflow.com/questions/33406169/failed-to-execute-setselectionrange-on-htmlinputelement-the-input-elements
|
|
||||||
target.type = "text";
|
|
||||||
|
|
||||||
target.setSelectionRange(pos, endPos ?? pos);
|
|
||||||
target.type = "number";
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
|
||||||
if (!props.onSetRating) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const setRating = editing ? setValueStage : props.onSetRating;
|
function nonStepChange() {
|
||||||
|
|
||||||
let val = e.target.value;
|
|
||||||
if (!useValidation.current) {
|
|
||||||
e.target.value = Number(val).toFixed(1);
|
|
||||||
const tempVal = Number(val) * 10;
|
|
||||||
setRating(tempVal || null);
|
|
||||||
useValidation.current = true;
|
useValidation.current = true;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const match = /(\d?)(\d?)(.?)((\d)?)/g.exec(val);
|
function setCursorPosition(
|
||||||
const matchOld = /(\d?)(\d?)(.?)((\d{0,2})?)/g.exec(text ?? "");
|
target: HTMLInputElement,
|
||||||
|
pos: number,
|
||||||
|
endPos?: number
|
||||||
|
) {
|
||||||
|
// This is a workaround to a missing feature where you can't set cursor position in input numbers.
|
||||||
|
// See https://stackoverflow.com/questions/33406169/failed-to-execute-setselectionrange-on-htmlinputelement-the-input-elements
|
||||||
|
target.type = "text";
|
||||||
|
|
||||||
if (match == null) {
|
target.setSelectionRange(pos, endPos ?? pos);
|
||||||
return;
|
target.type = "number";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (match[2] && !(match[2] == "0" && match[1] == "1")) {
|
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
match[2] = "";
|
if (!props.onSetRating) {
|
||||||
}
|
return;
|
||||||
if (match[4] == null || match[4] == "") {
|
|
||||||
match[4] = "0";
|
|
||||||
}
|
|
||||||
|
|
||||||
let value = match[1] + match[2] + "." + match[4];
|
|
||||||
e.target.value = value;
|
|
||||||
|
|
||||||
if (val.length > 0) {
|
|
||||||
if (Number(value) > 10) {
|
|
||||||
value = "10.0";
|
|
||||||
}
|
|
||||||
e.target.value = Number(value).toFixed(1);
|
|
||||||
let tempVal = Number(value) * 10;
|
|
||||||
setRating(tempVal || null);
|
|
||||||
|
|
||||||
let cursorPosition = 0;
|
|
||||||
if (match[2] && !match[4]) {
|
|
||||||
cursorPosition = 3;
|
|
||||||
} else if (matchOld != null && match[1] !== matchOld[1]) {
|
|
||||||
cursorPosition = 2;
|
|
||||||
} else if (
|
|
||||||
matchOld != null &&
|
|
||||||
match[1] === matchOld[1] &&
|
|
||||||
match[2] === matchOld[2] &&
|
|
||||||
match[4] === matchOld[4]
|
|
||||||
) {
|
|
||||||
cursorPosition = 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setCursorPosition(e.target, cursorPosition);
|
const setRating = editing ? setValueStage : props.onSetRating;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onBlur() {
|
let val = e.target.value;
|
||||||
if (editing) {
|
if (!useValidation.current) {
|
||||||
setEditing(false);
|
e.target.value = Number(val).toFixed(1);
|
||||||
if (props.onSetRating && valueStage !== props.value) {
|
const tempVal = Number(val) * 10;
|
||||||
props.onSetRating(valueStage);
|
setRating(tempVal || null);
|
||||||
|
useValidation.current = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = /(\d?)(\d?)(.?)((\d)?)/g.exec(val);
|
||||||
|
const matchOld = /(\d?)(\d?)(.?)((\d{0,2})?)/g.exec(text ?? "");
|
||||||
|
|
||||||
|
if (match == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match[2] && !(match[2] == "0" && match[1] == "1")) {
|
||||||
|
match[2] = "";
|
||||||
|
}
|
||||||
|
if (match[4] == null || match[4] == "") {
|
||||||
|
match[4] = "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = match[1] + match[2] + "." + match[4];
|
||||||
|
e.target.value = value;
|
||||||
|
|
||||||
|
if (val.length > 0) {
|
||||||
|
if (Number(value) > 10) {
|
||||||
|
value = "10.0";
|
||||||
|
}
|
||||||
|
e.target.value = Number(value).toFixed(1);
|
||||||
|
let tempVal = Number(value) * 10;
|
||||||
|
setRating(tempVal || null);
|
||||||
|
|
||||||
|
let cursorPosition = 0;
|
||||||
|
if (match[2] && !match[4]) {
|
||||||
|
cursorPosition = 3;
|
||||||
|
} else if (matchOld != null && match[1] !== matchOld[1]) {
|
||||||
|
cursorPosition = 2;
|
||||||
|
} else if (
|
||||||
|
matchOld != null &&
|
||||||
|
match[1] === matchOld[1] &&
|
||||||
|
match[2] === matchOld[2] &&
|
||||||
|
match[4] === matchOld[4]
|
||||||
|
) {
|
||||||
|
cursorPosition = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCursorPosition(e.target, cursorPosition);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!showTextField) {
|
function onBlur() {
|
||||||
return (
|
if (editing) {
|
||||||
<div className="rating-number disabled">
|
setEditing(false);
|
||||||
{props.withoutContext && <Icon icon={faStar} />}
|
if (props.onSetRating && valueStage !== props.value) {
|
||||||
<span>{Number((effectiveValue ?? 0) / 10).toFixed(1)}</span>
|
props.onSetRating(valueStage);
|
||||||
{!props.disabled && props.clickToRate && (
|
}
|
||||||
<Button
|
}
|
||||||
variant="minimal"
|
}
|
||||||
size="sm"
|
|
||||||
className="edit-rating-button"
|
if (!showTextField) {
|
||||||
onClick={() => setEditing(true)}
|
return (
|
||||||
>
|
<div className="rating-number disabled">
|
||||||
<Icon className="text-primary" icon={faPencil} />
|
{props.withoutContext && <Icon icon={faStar} />}
|
||||||
</Button>
|
<span>{Number((effectiveValue ?? 0) / 10).toFixed(1)}</span>
|
||||||
)}
|
{!props.disabled && props.clickToRate && (
|
||||||
</div>
|
<Button
|
||||||
);
|
variant="minimal"
|
||||||
} else {
|
size="sm"
|
||||||
return (
|
className="edit-rating-button"
|
||||||
<div className="rating-number">
|
onClick={() => setEditing(true)}
|
||||||
<input
|
>
|
||||||
ref={ratingRef}
|
<Icon className="text-primary" icon={faPencil} />
|
||||||
className="text-input form-control"
|
</Button>
|
||||||
name="ratingnumber"
|
)}
|
||||||
type="number"
|
</div>
|
||||||
onMouseDown={stepChange}
|
);
|
||||||
onKeyDown={nonStepChange}
|
} else {
|
||||||
onChange={handleChange}
|
return (
|
||||||
onBlur={onBlur}
|
<div className="rating-number">
|
||||||
value={text}
|
<input
|
||||||
min="0.0"
|
ref={ratingRef}
|
||||||
step="0.1"
|
className="text-input form-control"
|
||||||
max="10"
|
name="ratingnumber"
|
||||||
placeholder="0.0"
|
type="number"
|
||||||
/>
|
onMouseDown={stepChange}
|
||||||
</div>
|
onKeyDown={nonStepChange}
|
||||||
);
|
onChange={handleChange}
|
||||||
|
onBlur={onBlur}
|
||||||
|
value={text}
|
||||||
|
min="0.0"
|
||||||
|
step="0.1"
|
||||||
|
max="10"
|
||||||
|
placeholder="0.0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Button } from "react-bootstrap";
|
import { Button } from "react-bootstrap";
|
||||||
import { Icon } from "../Icon";
|
import { Icon } from "../Icon";
|
||||||
import { faStar as fasStar } from "@fortawesome/free-solid-svg-icons";
|
import { faStar as fasStar } from "@fortawesome/free-solid-svg-icons";
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
RatingSystemType,
|
RatingSystemType,
|
||||||
} from "src/utils/rating";
|
} from "src/utils/rating";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
|
import { PatchComponent } from "src/patch";
|
||||||
|
|
||||||
export interface IRatingStarsProps {
|
export interface IRatingStarsProps {
|
||||||
value: number | null;
|
value: number | null;
|
||||||
@@ -20,234 +21,235 @@ export interface IRatingStarsProps {
|
|||||||
valueRequired?: boolean;
|
valueRequired?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RatingStars: React.FC<IRatingStarsProps> = (
|
export const RatingStars = PatchComponent(
|
||||||
props: IRatingStarsProps
|
"RatingStars",
|
||||||
) => {
|
(props: IRatingStarsProps) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const [hoverRating, setHoverRating] = useState<number | undefined>();
|
const [hoverRating, setHoverRating] = useState<number | undefined>();
|
||||||
const disabled = props.disabled || !props.onSetRating;
|
const disabled = props.disabled || !props.onSetRating;
|
||||||
|
|
||||||
const rating = convertToRatingFormat(props.value, {
|
const rating = convertToRatingFormat(props.value, {
|
||||||
type: RatingSystemType.Stars,
|
type: RatingSystemType.Stars,
|
||||||
starPrecision: props.precision,
|
starPrecision: props.precision,
|
||||||
});
|
});
|
||||||
const stars = rating ? Math.floor(rating) : 0;
|
const stars = rating ? Math.floor(rating) : 0;
|
||||||
// the upscaling was necesary to fix rounding issue present with tenth place precision
|
// the upscaling was necesary to fix rounding issue present with tenth place precision
|
||||||
const fraction = rating ? ((rating * 10) % 10) / 10 : 0;
|
const fraction = rating ? ((rating * 10) % 10) / 10 : 0;
|
||||||
|
|
||||||
const max = 5;
|
const max = 5;
|
||||||
const precision = getRatingPrecision(props.precision);
|
const precision = getRatingPrecision(props.precision);
|
||||||
|
|
||||||
function newToggleFraction() {
|
function newToggleFraction() {
|
||||||
if (precision !== 1) {
|
if (precision !== 1) {
|
||||||
if (fraction !== precision) {
|
if (fraction !== precision) {
|
||||||
if (fraction == 0) {
|
if (fraction == 0) {
|
||||||
return 1 - precision;
|
return 1 - precision;
|
||||||
}
|
|
||||||
|
|
||||||
return fraction - precision;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setRating(thisStar: number) {
|
|
||||||
if (!props.onSetRating) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let newRating: number | undefined = thisStar;
|
|
||||||
|
|
||||||
// toggle rating fraction if we're clicking on the current rating
|
|
||||||
if (
|
|
||||||
(stars === thisStar && !fraction) ||
|
|
||||||
(stars + 1 === thisStar && fraction)
|
|
||||||
) {
|
|
||||||
const f = newToggleFraction();
|
|
||||||
if (!f) {
|
|
||||||
if (props.valueRequired) {
|
|
||||||
if (fraction) {
|
|
||||||
newRating = stars + 1;
|
|
||||||
} else {
|
|
||||||
newRating = stars;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return fraction - precision;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRating(thisStar: number) {
|
||||||
|
if (!props.onSetRating) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newRating: number | undefined = thisStar;
|
||||||
|
|
||||||
|
// toggle rating fraction if we're clicking on the current rating
|
||||||
|
if (
|
||||||
|
(stars === thisStar && !fraction) ||
|
||||||
|
(stars + 1 === thisStar && fraction)
|
||||||
|
) {
|
||||||
|
const f = newToggleFraction();
|
||||||
|
if (!f) {
|
||||||
|
if (props.valueRequired) {
|
||||||
|
if (fraction) {
|
||||||
|
newRating = stars + 1;
|
||||||
|
} else {
|
||||||
|
newRating = stars;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newRating = undefined;
|
||||||
|
}
|
||||||
|
} else if (fraction) {
|
||||||
|
// we're toggling from an existing fraction so use the stars value
|
||||||
|
newRating = stars + f;
|
||||||
} else {
|
} else {
|
||||||
newRating = undefined;
|
// we're toggling from a whole value, so decrement from current rating
|
||||||
|
newRating = stars - 1 + f;
|
||||||
}
|
}
|
||||||
} else if (fraction) {
|
|
||||||
// we're toggling from an existing fraction so use the stars value
|
|
||||||
newRating = stars + f;
|
|
||||||
} else {
|
|
||||||
// we're toggling from a whole value, so decrement from current rating
|
|
||||||
newRating = stars - 1 + f;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// set the hover rating to undefined so that it doesn't immediately clear
|
// set the hover rating to undefined so that it doesn't immediately clear
|
||||||
// the stars
|
// the stars
|
||||||
setHoverRating(undefined);
|
|
||||||
|
|
||||||
if (!newRating) {
|
|
||||||
props.onSetRating(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
props.onSetRating(
|
|
||||||
convertFromRatingFormat(newRating, RatingSystemType.Stars)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMouseOver(thisStar: number) {
|
|
||||||
if (!disabled) {
|
|
||||||
setHoverRating(thisStar);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMouseOut(thisStar: number) {
|
|
||||||
if (!disabled && hoverRating === thisStar) {
|
|
||||||
setHoverRating(undefined);
|
setHoverRating(undefined);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getClassName(thisStar: number) {
|
if (!newRating) {
|
||||||
if (hoverRating && hoverRating >= thisStar) {
|
props.onSetRating(null);
|
||||||
if (hoverRating === stars) {
|
return;
|
||||||
return "unsetting";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return "setting";
|
props.onSetRating(
|
||||||
|
convertFromRatingFormat(newRating, RatingSystemType.Stars)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stars && stars >= thisStar) {
|
function onMouseOver(thisStar: number) {
|
||||||
return "set";
|
if (!disabled) {
|
||||||
}
|
setHoverRating(thisStar);
|
||||||
|
|
||||||
return "unset";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTooltip(thisStar: number, current: RatingFraction | undefined) {
|
|
||||||
if (disabled) {
|
|
||||||
if (rating) {
|
|
||||||
// always return current rating for disabled control
|
|
||||||
return rating.toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// adjust tooltip to use fractions
|
function onMouseOut(thisStar: number) {
|
||||||
if (!current) {
|
if (!disabled && hoverRating === thisStar) {
|
||||||
return intl.formatMessage({ id: "actions.unset" });
|
setHoverRating(undefined);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (current.rating + current.fraction).toString();
|
function getClassName(thisStar: number) {
|
||||||
}
|
if (hoverRating && hoverRating >= thisStar) {
|
||||||
|
if (hoverRating === stars) {
|
||||||
type RatingFraction = {
|
return "unsetting";
|
||||||
rating: number;
|
|
||||||
fraction: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
function getCurrentSelectedRating(): RatingFraction | undefined {
|
|
||||||
let r: number = hoverRating ? hoverRating : stars;
|
|
||||||
let f: number | undefined = fraction;
|
|
||||||
|
|
||||||
if (hoverRating) {
|
|
||||||
if (hoverRating === stars && precision === 1) {
|
|
||||||
if (props.valueRequired) {
|
|
||||||
return { rating: r, fraction: 0 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// unsetting
|
return "setting";
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
if (hoverRating === stars + 1 && fraction && fraction === precision) {
|
|
||||||
if (props.valueRequired) {
|
if (stars && stars >= thisStar) {
|
||||||
return { rating: r, fraction: 0 };
|
return "set";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unset";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTooltip(thisStar: number, current: RatingFraction | undefined) {
|
||||||
|
if (disabled) {
|
||||||
|
if (rating) {
|
||||||
|
// always return current rating for disabled control
|
||||||
|
return rating.toString();
|
||||||
}
|
}
|
||||||
// unsetting
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (f && hoverRating === stars + 1) {
|
// adjust tooltip to use fractions
|
||||||
f = newToggleFraction();
|
if (!current) {
|
||||||
r--;
|
return intl.formatMessage({ id: "actions.unset" });
|
||||||
} else if (!f && hoverRating === stars) {
|
|
||||||
f = newToggleFraction();
|
|
||||||
r--;
|
|
||||||
} else {
|
|
||||||
f = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (current.rating + current.fraction).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return { rating: r, fraction: f ?? 0 };
|
type RatingFraction = {
|
||||||
}
|
rating: number;
|
||||||
|
fraction: number;
|
||||||
|
};
|
||||||
|
|
||||||
function getButtonClassName(
|
function getCurrentSelectedRating(): RatingFraction | undefined {
|
||||||
thisStar: number,
|
let r: number = hoverRating ? hoverRating : stars;
|
||||||
current: RatingFraction | undefined
|
let f: number | undefined = fraction;
|
||||||
) {
|
|
||||||
if (!current || thisStar > current.rating + 1) {
|
if (hoverRating) {
|
||||||
return "star-fill-0";
|
if (hoverRating === stars && precision === 1) {
|
||||||
|
if (props.valueRequired) {
|
||||||
|
return { rating: r, fraction: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// unsetting
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (hoverRating === stars + 1 && fraction && fraction === precision) {
|
||||||
|
if (props.valueRequired) {
|
||||||
|
return { rating: r, fraction: 0 };
|
||||||
|
}
|
||||||
|
// unsetting
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (f && hoverRating === stars + 1) {
|
||||||
|
f = newToggleFraction();
|
||||||
|
r--;
|
||||||
|
} else if (!f && hoverRating === stars) {
|
||||||
|
f = newToggleFraction();
|
||||||
|
r--;
|
||||||
|
} else {
|
||||||
|
f = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { rating: r, fraction: f ?? 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (thisStar <= current.rating) {
|
function getButtonClassName(
|
||||||
return "star-fill-100";
|
thisStar: number,
|
||||||
}
|
current: RatingFraction | undefined
|
||||||
|
|
||||||
let w = current.fraction * 100;
|
|
||||||
return `star-fill-${w}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderRatingButton = (thisStar: number) => {
|
|
||||||
const ratingFraction = getCurrentSelectedRating();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
disabled={disabled}
|
|
||||||
className={`minimal ${getButtonClassName(thisStar, ratingFraction)}`}
|
|
||||||
onClick={() => setRating(thisStar)}
|
|
||||||
variant="secondary"
|
|
||||||
onMouseEnter={() => onMouseOver(thisStar)}
|
|
||||||
onMouseLeave={() => onMouseOut(thisStar)}
|
|
||||||
onFocus={() => onMouseOver(thisStar)}
|
|
||||||
onBlur={() => onMouseOut(thisStar)}
|
|
||||||
title={getTooltip(thisStar, ratingFraction)}
|
|
||||||
key={`star-${thisStar}`}
|
|
||||||
>
|
|
||||||
<div className="filled-star">
|
|
||||||
<Icon icon={fasStar} className="set" />
|
|
||||||
</div>
|
|
||||||
<div className="unfilled-star">
|
|
||||||
<Icon icon={farStar} className={getClassName(thisStar)} />
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const maybeRenderStarRatingNumber = () => {
|
|
||||||
const ratingFraction = getCurrentSelectedRating();
|
|
||||||
if (
|
|
||||||
!ratingFraction ||
|
|
||||||
(ratingFraction.rating == 0 && ratingFraction.fraction == 0)
|
|
||||||
) {
|
) {
|
||||||
return;
|
if (!current || thisStar > current.rating + 1) {
|
||||||
|
return "star-fill-0";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thisStar <= current.rating) {
|
||||||
|
return "star-fill-100";
|
||||||
|
}
|
||||||
|
|
||||||
|
let w = current.fraction * 100;
|
||||||
|
return `star-fill-${w}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderRatingButton = (thisStar: number) => {
|
||||||
|
const ratingFraction = getCurrentSelectedRating();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
disabled={disabled}
|
||||||
|
className={`minimal ${getButtonClassName(thisStar, ratingFraction)}`}
|
||||||
|
onClick={() => setRating(thisStar)}
|
||||||
|
variant="secondary"
|
||||||
|
onMouseEnter={() => onMouseOver(thisStar)}
|
||||||
|
onMouseLeave={() => onMouseOut(thisStar)}
|
||||||
|
onFocus={() => onMouseOver(thisStar)}
|
||||||
|
onBlur={() => onMouseOut(thisStar)}
|
||||||
|
title={getTooltip(thisStar, ratingFraction)}
|
||||||
|
key={`star-${thisStar}`}
|
||||||
|
>
|
||||||
|
<div className="filled-star">
|
||||||
|
<Icon icon={fasStar} className="set" />
|
||||||
|
</div>
|
||||||
|
<div className="unfilled-star">
|
||||||
|
<Icon icon={farStar} className={getClassName(thisStar)} />
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const maybeRenderStarRatingNumber = () => {
|
||||||
|
const ratingFraction = getCurrentSelectedRating();
|
||||||
|
if (
|
||||||
|
!ratingFraction ||
|
||||||
|
(ratingFraction.rating == 0 && ratingFraction.fraction == 0)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="star-rating-number">
|
||||||
|
{ratingFraction.rating + ratingFraction.fraction}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const precisionClassName = `rating-stars-precision-${props.precision}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="star-rating-number">
|
<div className={`rating-stars ${precisionClassName}`}>
|
||||||
{ratingFraction.rating + ratingFraction.fraction}
|
{Array.from(Array(max)).map((value, index) =>
|
||||||
</span>
|
renderRatingButton(index + 1)
|
||||||
|
)}
|
||||||
|
{maybeRenderStarRatingNumber()}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
);
|
||||||
const precisionClassName = `rating-stars-precision-${props.precision}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`rating-stars ${precisionClassName}`}>
|
|
||||||
{Array.from(Array(max)).map((value, index) =>
|
|
||||||
renderRatingButton(index + 1)
|
|
||||||
)}
|
|
||||||
{maybeRenderStarRatingNumber()}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
} from "src/utils/rating";
|
} from "src/utils/rating";
|
||||||
import { RatingNumber } from "./RatingNumber";
|
import { RatingNumber } from "./RatingNumber";
|
||||||
import { RatingStars } from "./RatingStars";
|
import { RatingStars } from "./RatingStars";
|
||||||
|
import { PatchComponent } from "src/patch";
|
||||||
|
|
||||||
export interface IRatingSystemProps {
|
export interface IRatingSystemProps {
|
||||||
value: number | null | undefined;
|
value: number | null | undefined;
|
||||||
@@ -19,34 +20,35 @@ export interface IRatingSystemProps {
|
|||||||
withoutContext?: boolean;
|
withoutContext?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RatingSystem: React.FC<IRatingSystemProps> = (
|
export const RatingSystem = PatchComponent(
|
||||||
props: IRatingSystemProps
|
"RatingSystem",
|
||||||
) => {
|
(props: IRatingSystemProps) => {
|
||||||
const { configuration: config } = React.useContext(ConfigurationContext);
|
const { configuration: config } = React.useContext(ConfigurationContext);
|
||||||
const ratingSystemOptions =
|
const ratingSystemOptions =
|
||||||
config?.ui.ratingSystemOptions ?? defaultRatingSystemOptions;
|
config?.ui.ratingSystemOptions ?? defaultRatingSystemOptions;
|
||||||
|
|
||||||
if (ratingSystemOptions.type === RatingSystemType.Stars) {
|
if (ratingSystemOptions.type === RatingSystemType.Stars) {
|
||||||
return (
|
return (
|
||||||
<RatingStars
|
<RatingStars
|
||||||
value={props.value ?? null}
|
value={props.value ?? null}
|
||||||
onSetRating={props.onSetRating}
|
onSetRating={props.onSetRating}
|
||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
precision={
|
precision={
|
||||||
ratingSystemOptions.starPrecision ?? defaultRatingStarPrecision
|
ratingSystemOptions.starPrecision ?? defaultRatingStarPrecision
|
||||||
}
|
}
|
||||||
valueRequired={props.valueRequired}
|
valueRequired={props.valueRequired}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<RatingNumber
|
<RatingNumber
|
||||||
value={props.value ?? null}
|
value={props.value ?? null}
|
||||||
onSetRating={props.onSetRating}
|
onSetRating={props.onSetRating}
|
||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
clickToRate={props.clickToRate}
|
clickToRate={props.clickToRate}
|
||||||
withoutContext={props.withoutContext}
|
withoutContext={props.withoutContext}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
);
|
||||||
|
|||||||
@@ -196,6 +196,9 @@ Returns `void`.
|
|||||||
- `PerformerImagesPanel`
|
- `PerformerImagesPanel`
|
||||||
- `PerformerScenesPanel`
|
- `PerformerScenesPanel`
|
||||||
- `PluginRoutes`
|
- `PluginRoutes`
|
||||||
|
- `RatingNumber`
|
||||||
|
- `RatingStars`
|
||||||
|
- `RatingSystem`
|
||||||
- `SceneCard`
|
- `SceneCard`
|
||||||
- `SceneCard.Details`
|
- `SceneCard.Details`
|
||||||
- `SceneCard.Image`
|
- `SceneCard.Image`
|
||||||
|
|||||||
3
ui/v2.5/src/pluginApi.d.ts
vendored
3
ui/v2.5/src/pluginApi.d.ts
vendored
@@ -727,6 +727,9 @@ declare namespace PluginApi {
|
|||||||
"GalleryCard.Image": React.FC<any>;
|
"GalleryCard.Image": React.FC<any>;
|
||||||
"GalleryCard.Overlays": React.FC<any>;
|
"GalleryCard.Overlays": React.FC<any>;
|
||||||
"GalleryCard.Popovers": React.FC<any>;
|
"GalleryCard.Popovers": React.FC<any>;
|
||||||
|
RatingNumber: React.FC<any>;
|
||||||
|
RatingStars: React.FC<any>;
|
||||||
|
RatingSystem: React.FC<any>;
|
||||||
};
|
};
|
||||||
type PatchableComponentNames = keyof typeof components | string;
|
type PatchableComponentNames = keyof typeof components | string;
|
||||||
namespace utils {
|
namespace utils {
|
||||||
|
|||||||
Reference in New Issue
Block a user