diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx index ed85d9309..d7c1a55fa 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx @@ -36,6 +36,8 @@ import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import { GalleryScrapeDialog } from "./GalleryScrapeDialog"; import { faSyncAlt } from "@fortawesome/free-solid-svg-icons"; import { galleryTitle } from "src/core/galleries"; +import { useRatingKeybinds } from "src/hooks/keybinds"; +import { ConfigurationContext } from "src/hooks/Config"; interface IProps { isVisible: boolean; @@ -65,6 +67,8 @@ export const GalleryEditPanel: React.FC< })) ); + const { configuration: stashConfig } = React.useContext(ConfigurationContext); + const Scrapers = useListGalleryScrapers(); const [queryableScrapers, setQueryableScrapers] = useState([]); @@ -133,6 +137,12 @@ export const GalleryEditPanel: React.FC< ); } + useRatingKeybinds( + isVisible, + stashConfig?.ui.ratingSystemOptions.type, + setRating + ); + useEffect(() => { if (isVisible) { Mousetrap.bind("s s", () => { @@ -142,35 +152,9 @@ export const GalleryEditPanel: React.FC< onDelete(); }); - // numeric keypresses get caught by jwplayer, so blur the element - // if the rating sequence is started - Mousetrap.bind("r", () => { - if (document.activeElement instanceof HTMLElement) { - document.activeElement.blur(); - } - - Mousetrap.bind("0", () => setRating(NaN)); - Mousetrap.bind("1", () => setRating(20)); - Mousetrap.bind("2", () => setRating(40)); - Mousetrap.bind("3", () => setRating(60)); - Mousetrap.bind("4", () => setRating(80)); - Mousetrap.bind("5", () => setRating(100)); - - setTimeout(() => { - Mousetrap.unbind("0"); - Mousetrap.unbind("1"); - Mousetrap.unbind("2"); - Mousetrap.unbind("3"); - Mousetrap.unbind("4"); - Mousetrap.unbind("5"); - }, 1000); - }); - return () => { Mousetrap.unbind("s s"); Mousetrap.unbind("d d"); - - Mousetrap.unbind("r"); }; } }); diff --git a/ui/v2.5/src/components/Images/ImageDetails/ImageEditPanel.tsx b/ui/v2.5/src/components/Images/ImageDetails/ImageEditPanel.tsx index 77b3dc02d..6259db41a 100644 --- a/ui/v2.5/src/components/Images/ImageDetails/ImageEditPanel.tsx +++ b/ui/v2.5/src/components/Images/ImageDetails/ImageEditPanel.tsx @@ -17,6 +17,8 @@ import { FormUtils } from "src/utils"; import { useFormik } from "formik"; import { Prompt } from "react-router-dom"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; +import { useRatingKeybinds } from "src/hooks/keybinds"; +import { ConfigurationContext } from "src/hooks/Config"; interface IProps { image: GQL.ImageDataFragment; @@ -35,6 +37,8 @@ export const ImageEditPanel: React.FC = ({ // Network state const [isLoading, setIsLoading] = useState(false); + const { configuration } = React.useContext(ConfigurationContext); + const [updateImage] = useImageUpdate(); const schema = yup.object({ @@ -69,6 +73,12 @@ export const ImageEditPanel: React.FC = ({ formik.setFieldValue("rating100", v); } + useRatingKeybinds( + true, + configuration?.ui.ratingSystemOptions.type, + setRating + ); + useEffect(() => { if (isVisible) { Mousetrap.bind("s s", () => { @@ -78,35 +88,9 @@ export const ImageEditPanel: React.FC = ({ onDelete(); }); - // numeric keypresses get caught by jwplayer, so blur the element - // if the rating sequence is started - Mousetrap.bind("r", () => { - if (document.activeElement instanceof HTMLElement) { - document.activeElement.blur(); - } - - Mousetrap.bind("0", () => setRating(NaN)); - Mousetrap.bind("1", () => setRating(20)); - Mousetrap.bind("2", () => setRating(40)); - Mousetrap.bind("3", () => setRating(60)); - Mousetrap.bind("4", () => setRating(80)); - Mousetrap.bind("5", () => setRating(100)); - - setTimeout(() => { - Mousetrap.unbind("0"); - Mousetrap.unbind("1"); - Mousetrap.unbind("2"); - Mousetrap.unbind("3"); - Mousetrap.unbind("4"); - Mousetrap.unbind("5"); - }, 1000); - }); - return () => { Mousetrap.unbind("s s"); Mousetrap.unbind("d d"); - - Mousetrap.unbind("r"); }; } }); diff --git a/ui/v2.5/src/components/Movies/MovieDetails/MovieEditPanel.tsx b/ui/v2.5/src/components/Movies/MovieDetails/MovieEditPanel.tsx index 07eb87648..7acb6c93e 100644 --- a/ui/v2.5/src/components/Movies/MovieDetails/MovieEditPanel.tsx +++ b/ui/v2.5/src/components/Movies/MovieDetails/MovieEditPanel.tsx @@ -21,6 +21,8 @@ import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import { useFormik } from "formik"; import { Prompt } from "react-router-dom"; import { MovieScrapeDialog } from "./MovieScrapeDialog"; +import { useRatingKeybinds } from "src/hooks/keybinds"; +import { ConfigurationContext } from "src/hooks/Config"; interface IMovieEditPanel { movie?: Partial; @@ -45,6 +47,7 @@ export const MovieEditPanel: React.FC = ({ }) => { const intl = useIntl(); const Toast = useToast(); + const { configuration: stashConfig } = React.useContext(ConfigurationContext); const isNew = movie === undefined; @@ -119,14 +122,10 @@ export const MovieEditPanel: React.FC = ({ formik.setFieldValue("rating100", v); } + useRatingKeybinds(true, stashConfig?.ui.ratingSystemOptions.type, setRating); + // set up hotkeys useEffect(() => { - Mousetrap.bind("r 0", () => setRating(NaN)); - Mousetrap.bind("r 1", () => setRating(20)); - Mousetrap.bind("r 2", () => setRating(40)); - Mousetrap.bind("r 3", () => setRating(60)); - Mousetrap.bind("r 4", () => setRating(80)); - Mousetrap.bind("r 5", () => setRating(100)); // Mousetrap.bind("u", (e) => { // setStudioFocus() // e.preventDefault(); @@ -134,12 +133,6 @@ export const MovieEditPanel: React.FC = ({ Mousetrap.bind("s s", () => formik.handleSubmit()); return () => { - Mousetrap.unbind("r 0"); - Mousetrap.unbind("r 1"); - Mousetrap.unbind("r 2"); - Mousetrap.unbind("r 3"); - Mousetrap.unbind("r 4"); - Mousetrap.unbind("r 5"); // Mousetrap.unbind("u"); Mousetrap.unbind("s s"); }; diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx index ddc29a08a..5853e6e55 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx @@ -39,6 +39,7 @@ import { faLink, } from "@fortawesome/free-solid-svg-icons"; import { IUIConfig } from "src/core/config"; +import { useRatingKeybinds } from "src/hooks/keybinds"; interface IProps { performer: GQL.PerformerDataFragment; @@ -110,6 +111,12 @@ const PerformerPage: React.FC = ({ performer }) => { } } + useRatingKeybinds( + true, + configuration?.ui.ratingSystemOptions.type, + setRating + ); + // set up hotkeys useEffect(() => { Mousetrap.bind("a", () => setActiveTabKey("details")); @@ -119,30 +126,6 @@ const PerformerPage: React.FC = ({ performer }) => { Mousetrap.bind("m", () => setActiveTabKey("movies")); Mousetrap.bind("f", () => setFavorite(!performer.favorite)); - // numeric keypresses get caught by jwplayer, so blur the element - // if the rating sequence is started - Mousetrap.bind("r", () => { - if (document.activeElement instanceof HTMLElement) { - document.activeElement.blur(); - } - - Mousetrap.bind("0", () => setRating(NaN)); - Mousetrap.bind("1", () => setRating(20)); - Mousetrap.bind("2", () => setRating(40)); - Mousetrap.bind("3", () => setRating(60)); - Mousetrap.bind("4", () => setRating(80)); - Mousetrap.bind("5", () => setRating(100)); - - setTimeout(() => { - Mousetrap.unbind("0"); - Mousetrap.unbind("1"); - Mousetrap.unbind("2"); - Mousetrap.unbind("3"); - Mousetrap.unbind("4"); - Mousetrap.unbind("5"); - }, 1000); - }); - return () => { Mousetrap.unbind("a"); Mousetrap.unbind("e"); diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx index adcf35048..48f0b32aa 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx @@ -47,6 +47,7 @@ import { faTrashAlt, } from "@fortawesome/free-solid-svg-icons"; import { objectTitle } from "src/core/files"; +import { useRatingKeybinds } from "src/hooks/keybinds"; const SceneScrapeDialog = lazy(() => import("./SceneScrapeDialog")); const SceneQueryModal = lazy(() => import("./SceneQueryModal")); @@ -187,6 +188,12 @@ export const SceneEditPanel: React.FC = ({ ); } + useRatingKeybinds( + isVisible, + stashConfig?.ui.ratingSystemOptions.type, + setRating + ); + useEffect(() => { if (isVisible) { Mousetrap.bind("s s", () => { @@ -198,35 +205,9 @@ export const SceneEditPanel: React.FC = ({ } }); - // numeric keypresses get caught by jwplayer, so blur the element - // if the rating sequence is started - Mousetrap.bind("r", () => { - if (document.activeElement instanceof HTMLElement) { - document.activeElement.blur(); - } - - Mousetrap.bind("0", () => setRating(NaN)); - Mousetrap.bind("1", () => setRating(20)); - Mousetrap.bind("2", () => setRating(40)); - Mousetrap.bind("3", () => setRating(60)); - Mousetrap.bind("4", () => setRating(80)); - Mousetrap.bind("5", () => setRating(100)); - - setTimeout(() => { - Mousetrap.unbind("0"); - Mousetrap.unbind("1"); - Mousetrap.unbind("2"); - Mousetrap.unbind("3"); - Mousetrap.unbind("4"); - Mousetrap.unbind("5"); - }, 1000); - }); - return () => { Mousetrap.unbind("s s"); Mousetrap.unbind("d d"); - - Mousetrap.unbind("r"); }; } }); diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioEditPanel.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioEditPanel.tsx index a5e5bf4d1..f0b8b622c 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/StudioEditPanel.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioEditPanel.tsx @@ -11,6 +11,8 @@ import { useFormik } from "formik"; import { Prompt } from "react-router-dom"; import { StringListInput } from "../../Shared/StringListInput"; import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; +import { useRatingKeybinds } from "src/hooks/keybinds"; +import { ConfigurationContext } from "src/hooks/Config"; interface IStudioEditPanel { studio: Partial; @@ -33,6 +35,8 @@ export const StudioEditPanel: React.FC = ({ }) => { const intl = useIntl(); + const { configuration } = React.useContext(ConfigurationContext); + const isNew = !studio || !studio.id; const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true); @@ -99,38 +103,18 @@ export const StudioEditPanel: React.FC = ({ return input; } + useRatingKeybinds( + true, + configuration?.ui.ratingSystemOptions.type, + setRating + ); + // set up hotkeys useEffect(() => { Mousetrap.bind("s s", () => formik.handleSubmit()); - // numeric keypresses get caught by jwplayer, so blur the element - // if the rating sequence is started - Mousetrap.bind("r", () => { - if (document.activeElement instanceof HTMLElement) { - document.activeElement.blur(); - } - - Mousetrap.bind("0", () => setRating(NaN)); - Mousetrap.bind("1", () => setRating(20)); - Mousetrap.bind("2", () => setRating(40)); - Mousetrap.bind("3", () => setRating(60)); - Mousetrap.bind("4", () => setRating(80)); - Mousetrap.bind("5", () => setRating(100)); - - setTimeout(() => { - Mousetrap.unbind("0"); - Mousetrap.unbind("1"); - Mousetrap.unbind("2"); - Mousetrap.unbind("3"); - Mousetrap.unbind("4"); - Mousetrap.unbind("5"); - }, 1000); - }); - return () => { Mousetrap.unbind("s s"); - - Mousetrap.unbind("e"); }; }); diff --git a/ui/v2.5/src/docs/en/Changelog/v0190.md b/ui/v2.5/src/docs/en/Changelog/v0190.md index 6584e2595..568050a3b 100644 --- a/ui/v2.5/src/docs/en/Changelog/v0190.md +++ b/ui/v2.5/src/docs/en/Changelog/v0190.md @@ -10,6 +10,7 @@ * Added Anonymise task to generate an anonymised version of the database. ([#3186](https://github.com/stashapp/stash/pull/3186)) ### 🎨 Improvements +* Added `r x x` keyboard shortcuts to set decimal ratings. ([#3226](https://github.com/stashapp/stash/pull/3226)) * Changed performer aliases to be a list, rather than a string field. ([#3113](https://github.com/stashapp/stash/pull/3113)) ### 🐛 Bug fixes diff --git a/ui/v2.5/src/docs/en/Manual/KeyboardShortcuts.md b/ui/v2.5/src/docs/en/Manual/KeyboardShortcuts.md index 29408438e..7eee67ef4 100644 --- a/ui/v2.5/src/docs/en/Manual/KeyboardShortcuts.md +++ b/ui/v2.5/src/docs/en/Manual/KeyboardShortcuts.md @@ -84,8 +84,10 @@ | Keyboard sequence | Action | |-------------------|--------| -| `r {1-5}` | Set rating | -| `r 0` | Unset rating | +| `r {1-5}` | Set rating (stars) | +| `r 0` | Unset rating (stars) | +| `r {0-9} {0-9}` | Set rating (decimal - `00` for `10.0`) | +| ``r ` `` | Unset rating (decimal) | | `s s` | Save Scene | | `d d` | Delete Scene | | `Ctrl + v` | Paste Scene cover | @@ -110,8 +112,10 @@ | `e` | Edit Movie | | `s s` | Save Movie | | `d d` | Delete Movie | -| `r {1-5}` | Set rating (in edit mode) | -| `r 0` | Unset rating (in edit mode) | +| `r {1-5}` | [Edit mode] Set rating (stars) | +| `r 0` | [Edit mode] Unset rating (stars) | +| `r {0-9} {0-9}` | [Edit mode] Set rating (decimal - `r 0 0` for `10.0`) | +| ``r ` `` | [Edit mode] Unset rating (decimal) | | `Ctrl + v` | Paste Movie image | [//]: # "Commented until implementation is dealt with" diff --git a/ui/v2.5/src/hooks/keybinds.ts b/ui/v2.5/src/hooks/keybinds.ts new file mode 100644 index 000000000..dc85ce51d --- /dev/null +++ b/ui/v2.5/src/hooks/keybinds.ts @@ -0,0 +1,85 @@ +import Mousetrap from "mousetrap"; +import { useEffect, useRef } from "react"; +import { RatingSystemType } from "src/utils/rating"; + +export function useRatingKeybinds( + isVisible: boolean, + ratingSystem: RatingSystemType, + setRating: (v: number) => void +) { + const firstChar = useRef(undefined); + + const starRatingShortcuts: { [char: string]: number } = { + "0": NaN, + "1": 20, + "2": 40, + "3": 60, + "4": 80, + "5": 100, + }; + + function handleStarRatingKeybinds() { + for (const key in starRatingShortcuts) { + Mousetrap.bind(key, () => setRating(starRatingShortcuts[key])); + } + + setTimeout(() => { + for (const key in starRatingShortcuts) { + Mousetrap.unbind(key); + } + }, 1000); + } + + function handleDecimalKeybinds() { + Mousetrap.bind("`", () => { + setRating(NaN); + }); + + for (let i = 0; i <= 9; ++i) { + Mousetrap.bind(i.toString(), () => { + if (firstChar.current !== undefined) { + let combined = parseInt(firstChar.current + i.toString()); + if (combined === 0) { + combined = 100; + } + + setRating(combined); + firstChar.current = undefined; + } else { + firstChar.current = i.toString(); + } + }); + } + + setTimeout(() => { + firstChar.current = undefined; + + Mousetrap.unbind("`"); + for (let i = 0; i <= 9; ++i) { + Mousetrap.unbind(i.toString()); + } + }, 1000); + } + + useEffect(() => { + if (!isVisible) return; + + Mousetrap.bind("r", () => { + // numeric keypresses get caught by jwplayer, so blur the element + // if the rating sequence is started + if (document.activeElement instanceof HTMLElement) { + document.activeElement.blur(); + } + + if (ratingSystem === RatingSystemType.Stars) { + return handleStarRatingKeybinds(); + } else { + return handleDecimalKeybinds(); + } + }); + + return () => { + Mousetrap.unbind("r"); + }; + }); +}