From e6d9d385a7e16c56eab6bb4932e3068e6db9ee16 Mon Sep 17 00:00:00 2001 From: Infinite Date: Sat, 8 Feb 2020 16:54:20 +0100 Subject: [PATCH] Add O-counter (#334) --- ui/v2.5/src/components/Scenes/SceneCard.tsx | 19 +- .../Scenes/SceneDetails/OCounterButton.tsx | 67 +++++++ .../components/Scenes/SceneDetails/Scene.tsx | 61 +++++++ .../src/components/Shared/HoverPopover.tsx | 20 ++- ui/v2.5/src/components/Shared/SweatDrops.tsx | 10 ++ ui/v2.5/src/components/Shared/index.ts | 1 + ui/v2.5/src/core/StashService.ts | 18 ++ ui/v2.5/src/core/generated-graphql.tsx | 170 +++++++++++++++++- .../models/list-filter/criteria/criterion.ts | 3 + .../src/models/list-filter/criteria/utils.ts | 2 + ui/v2.5/src/models/list-filter/filter.ts | 9 +- 11 files changed, 369 insertions(+), 11 deletions(-) create mode 100644 ui/v2.5/src/components/Scenes/SceneDetails/OCounterButton.tsx create mode 100644 ui/v2.5/src/components/Shared/SweatDrops.tsx diff --git a/ui/v2.5/src/components/Scenes/SceneCard.tsx b/ui/v2.5/src/components/Scenes/SceneCard.tsx index 7d2793aa9..6a24cf5ff 100644 --- a/ui/v2.5/src/components/Scenes/SceneCard.tsx +++ b/ui/v2.5/src/components/Scenes/SceneCard.tsx @@ -5,7 +5,7 @@ import cx from "classnames"; import * as GQL from "src/core/generated-graphql"; import { StashService } from "src/core/StashService"; import { VideoHoverHook } from "src/hooks"; -import { Icon, TagLink, HoverPopover } from "src/components/Shared"; +import { Icon, TagLink, HoverPopover, SweatDrops } from "src/components/Shared"; import { TextUtils } from "src/utils"; interface ISceneCardProps { @@ -135,11 +135,25 @@ export const SceneCard: React.FC = ( ); } + function maybeRenderOCounter() { + if (props.scene.o_counter) { + return ( +
+ +
+ ) + } + } + function maybeRenderPopoverButtonGroup() { if ( props.scene.tags.length > 0 || props.scene.performers.length > 0 || - props.scene.scene_markers.length > 0 + props.scene.scene_markers.length > 0 || + props.scene?.o_counter ) { return ( <> @@ -148,6 +162,7 @@ export const SceneCard: React.FC = ( {maybeRenderTagPopoverButton()} {maybeRenderPerformerPopoverButton()} {maybeRenderSceneMarkerPopoverButton()} + {maybeRenderOCounter()} ); diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/OCounterButton.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/OCounterButton.tsx new file mode 100644 index 000000000..2d884bada --- /dev/null +++ b/ui/v2.5/src/components/Scenes/SceneDetails/OCounterButton.tsx @@ -0,0 +1,67 @@ +import React from "react"; +import { Button, Spinner } from 'react-bootstrap'; +import { Icon, HoverPopover, SweatDrops } from 'src/components/Shared'; + +export interface IOCounterButtonProps { + loading: boolean + value: number + onIncrement: () => void + onDecrement: () => void + onReset: () => void + onMenuOpened?: () => void + onMenuClosed?: () => void +} + +export const OCounterButton: React.FC = (props: IOCounterButtonProps) => { + if(props.loading) + return ; + + const renderButton = () => ( + + ); + + if (props.value) { + return ( + +
+ +
+
+ +
+ + } + enterDelay={1000} + placement="bottom" + onOpen={props.onMenuOpened} + onClose={props.onMenuClosed} + > + { renderButton() } +
+ ); + } + return renderButton(); +} diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx index c391ee62c..30c01457c 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx @@ -6,20 +6,27 @@ import * as GQL from "src/core/generated-graphql"; import { StashService } from "src/core/StashService"; import { GalleryViewer } from "src/components/Galleries/GalleryViewer"; import { LoadingIndicator } from "src/components/Shared"; +import { useToast } from 'src/hooks'; import { ScenePlayer } from "src/components/ScenePlayer"; import { ScenePerformerPanel } from "./ScenePerformerPanel"; import { SceneMarkersPanel } from "./SceneMarkersPanel"; import { SceneFileInfoPanel } from "./SceneFileInfoPanel"; import { SceneEditPanel } from "./SceneEditPanel"; import { SceneDetailPanel } from "./SceneDetailPanel"; +import { OCounterButton } from './OCounterButton'; export const Scene: React.FC = () => { const { id = "new" } = useParams(); const location = useLocation(); const history = useHistory(); + const Toast = useToast(); const [timestamp, setTimestamp] = useState(getInitialTimestamp()); const [scene, setScene] = useState(); const { data, error, loading } = StashService.useFindScene(id); + const [oLoading, setOLoading] = useState(false); + const [incrementO] = StashService.useSceneIncrementO(scene?.id ?? "0"); + const [decrementO] = StashService.useSceneDecrementO(scene?.id ?? "0"); + const [resetO] = StashService.useSceneResetO(scene?.id ?? "0"); const queryParams = queryString.parse(location.search); const autoplay = queryParams?.autoplay === "true"; @@ -37,6 +44,51 @@ export const Scene: React.FC = () => { ); } + const updateOCounter = (newValue: number) => { + const modifiedScene = { ...scene } as GQL.SceneDataFragment; + modifiedScene.o_counter = newValue; + setScene(modifiedScene); + } + + const onIncrementClick = async () => { + try { + setOLoading(true); + const result = await incrementO(); + if(result.data) + updateOCounter(result.data.sceneIncrementO); + } catch (e) { + Toast.error(e); + } finally { + setOLoading(false); + } + } + + const onDecrementClick = async () => { + try { + setOLoading(true); + const result = await decrementO(); + if(result.data) + updateOCounter(result.data.sceneDecrementO); + } catch (e) { + Toast.error(e); + } finally { + setOLoading(false); + } + } + + const onResetClick = async () => { + try { + setOLoading(true); + const result = await resetO(); + if(result.data) + updateOCounter(result.data.sceneResetO); + } catch (e) { + Toast.error(e); + } finally { + setOLoading(false); + } + } + function onClickMarker(marker: GQL.SceneMarkerDataFragment) { setTimestamp(marker.seconds); } @@ -51,6 +103,15 @@ export const Scene: React.FC = () => { <>
+
+ +
diff --git a/ui/v2.5/src/components/Shared/HoverPopover.tsx b/ui/v2.5/src/components/Shared/HoverPopover.tsx index 1941f4e4d..b991bfabc 100644 --- a/ui/v2.5/src/components/Shared/HoverPopover.tsx +++ b/ui/v2.5/src/components/Shared/HoverPopover.tsx @@ -7,6 +7,8 @@ interface IHoverPopover { content: JSX.Element[] | JSX.Element | string; className?: string; placement?: OverlayProps["placement"]; + onOpen?: () => void; + onClose?: () => void; } export const HoverPopover: React.FC = ({ @@ -15,7 +17,9 @@ export const HoverPopover: React.FC = ({ content, children, className, - placement = "top" + placement = "top", + onOpen, + onClose }) => { const [show, setShow] = useState(false); const triggerRef = useRef(null); @@ -24,13 +28,19 @@ export const HoverPopover: React.FC = ({ const handleMouseEnter = useCallback(() => { window.clearTimeout(leaveTimer.current); - enterTimer.current = window.setTimeout(() => setShow(true), enterDelay); - }, [enterDelay]); + enterTimer.current = window.setTimeout(() => { + setShow(true) + onOpen?.(); + }, enterDelay); + }, [enterDelay, onOpen]); const handleMouseLeave = useCallback(() => { window.clearTimeout(enterTimer.current); - leaveTimer.current = window.setTimeout(() => setShow(false), leaveDelay); - }, [leaveDelay]); + leaveTimer.current = window.setTimeout(() => { + setShow(false) + onClose?.(); + }, leaveDelay); + }, [leaveDelay, onClose]); useEffect( () => () => { diff --git a/ui/v2.5/src/components/Shared/SweatDrops.tsx b/ui/v2.5/src/components/Shared/SweatDrops.tsx new file mode 100644 index 000000000..22009d785 --- /dev/null +++ b/ui/v2.5/src/components/Shared/SweatDrops.tsx @@ -0,0 +1,10 @@ +import React from 'react'; + +export const SweatDrops = () => ( + + + +); diff --git a/ui/v2.5/src/components/Shared/index.ts b/ui/v2.5/src/components/Shared/index.ts index 368fb676b..f6f354003 100644 --- a/ui/v2.5/src/components/Shared/index.ts +++ b/ui/v2.5/src/components/Shared/index.ts @@ -16,3 +16,4 @@ export { TagLink } from "./TagLink"; export { HoverPopover } from "./HoverPopover"; export { default as LoadingIndicator } from "./LoadingIndicator"; export { ImageInput } from "./ImageInput"; +export { SweatDrops } from './SweatDrops'; diff --git a/ui/v2.5/src/core/StashService.ts b/ui/v2.5/src/core/StashService.ts index a0cae546f..ad3455025 100644 --- a/ui/v2.5/src/core/StashService.ts +++ b/ui/v2.5/src/core/StashService.ts @@ -369,6 +369,24 @@ export class StashService { return GQL.useScenesUpdateMutation({ variables: { input } }); } + public static useSceneIncrementO(id: string) { + return GQL.useSceneIncrementOMutation({ + variables: {id} + }); + } + + public static useSceneDecrementO(id: string) { + return GQL.useSceneDecrementOMutation({ + variables: {id} + }); + } + + public static useSceneResetO(id: string) { + return GQL.useSceneResetOMutation({ + variables: {id} + }); + } + public static useSceneDestroy(input: GQL.SceneDestroyInput) { return GQL.useSceneDestroyMutation({ variables: input, diff --git a/ui/v2.5/src/core/generated-graphql.tsx b/ui/v2.5/src/core/generated-graphql.tsx index d53a71fe5..85989430d 100644 --- a/ui/v2.5/src/core/generated-graphql.tsx +++ b/ui/v2.5/src/core/generated-graphql.tsx @@ -7,7 +7,7 @@ import * as ApolloReactHooks from '@apollo/react-hooks'; export type Maybe = T | null; export type Omit = Pick>; -// Generated in 2020-02-06T18:11:21+01:00 +// Generated in 2020-02-08T16:10:44+01:00 /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { @@ -271,6 +271,12 @@ export type Mutation = { bulkSceneUpdate?: Maybe>, sceneDestroy: Scalars['Boolean'], scenesUpdate?: Maybe>>, + /** Increments the o-counter for a scene. Returns the new value */ + sceneIncrementO: Scalars['Int'], + /** Decrements the o-counter for a scene. Returns the new value */ + sceneDecrementO: Scalars['Int'], + /** Resets the o-counter for a scene to 0. Returns the new value */ + sceneResetO: Scalars['Int'], sceneMarkerCreate?: Maybe, sceneMarkerUpdate?: Maybe, sceneMarkerDestroy: Scalars['Boolean'], @@ -309,6 +315,21 @@ export type MutationScenesUpdateArgs = { }; +export type MutationSceneIncrementOArgs = { + id: Scalars['ID'] +}; + + +export type MutationSceneDecrementOArgs = { + id: Scalars['ID'] +}; + + +export type MutationSceneResetOArgs = { + id: Scalars['ID'] +}; + + export type MutationSceneMarkerCreateArgs = { input: SceneMarkerCreateInput }; @@ -762,6 +783,7 @@ export type Scene = { url?: Maybe, date?: Maybe, rating?: Maybe, + o_counter?: Maybe, path: Scalars['String'], file: SceneFileType, paths: ScenePathsType, @@ -794,6 +816,8 @@ export type SceneFileType = { export type SceneFilterType = { /** Filter by rating */ rating?: Maybe, + /** Filter by o-counter */ + o_counter?: Maybe, /** Filter by resolution */ resolution?: Maybe, /** Filter by duration (in seconds) */ @@ -1189,7 +1213,7 @@ export type SceneMarkerDataFragment = ( export type SlimSceneDataFragment = ( { __typename?: 'Scene' } - & Pick + & Pick & { file: ( { __typename?: 'SceneFileType' } & Pick @@ -1216,7 +1240,7 @@ export type SlimSceneDataFragment = ( export type SceneDataFragment = ( { __typename?: 'Scene' } - & Pick + & Pick & { file: ( { __typename?: 'SceneFileType' } & Pick @@ -1492,6 +1516,36 @@ export type ScenesUpdateMutation = ( )>>> } ); +export type SceneIncrementOMutationVariables = { + id: Scalars['ID'] +}; + + +export type SceneIncrementOMutation = ( + { __typename?: 'Mutation' } + & Pick +); + +export type SceneDecrementOMutationVariables = { + id: Scalars['ID'] +}; + + +export type SceneDecrementOMutation = ( + { __typename?: 'Mutation' } + & Pick +); + +export type SceneResetOMutationVariables = { + id: Scalars['ID'] +}; + + +export type SceneResetOMutation = ( + { __typename?: 'Mutation' } + & Pick +); + export type SceneDestroyMutationVariables = { id: Scalars['ID'], delete_file?: Maybe, @@ -2226,6 +2280,7 @@ export const SlimSceneDataFragmentDoc = gql` url date rating + o_counter path file { size @@ -2356,6 +2411,7 @@ export const SceneDataFragmentDoc = gql` url date rating + o_counter path file { size @@ -2960,6 +3016,114 @@ export function useScenesUpdateMutation(baseOptions?: ApolloReactHooks.MutationH export type ScenesUpdateMutationHookResult = ReturnType; export type ScenesUpdateMutationResult = ApolloReactCommon.MutationResult; export type ScenesUpdateMutationOptions = ApolloReactCommon.BaseMutationOptions; +export const SceneIncrementODocument = gql` + mutation SceneIncrementO($id: ID!) { + sceneIncrementO(id: $id) +} + `; +export type SceneIncrementOMutationFn = ApolloReactCommon.MutationFunction; +export type SceneIncrementOComponentProps = Omit, 'mutation'>; + + export const SceneIncrementOComponent = (props: SceneIncrementOComponentProps) => ( + mutation={SceneIncrementODocument} {...props} /> + ); + + +/** + * __useSceneIncrementOMutation__ + * + * To run a mutation, you first call `useSceneIncrementOMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useSceneIncrementOMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [sceneIncrementOMutation, { data, loading, error }] = useSceneIncrementOMutation({ + * variables: { + * id: // value for 'id' + * }, + * }); + */ +export function useSceneIncrementOMutation(baseOptions?: ApolloReactHooks.MutationHookOptions) { + return ApolloReactHooks.useMutation(SceneIncrementODocument, baseOptions); + } +export type SceneIncrementOMutationHookResult = ReturnType; +export type SceneIncrementOMutationResult = ApolloReactCommon.MutationResult; +export type SceneIncrementOMutationOptions = ApolloReactCommon.BaseMutationOptions; +export const SceneDecrementODocument = gql` + mutation SceneDecrementO($id: ID!) { + sceneDecrementO(id: $id) +} + `; +export type SceneDecrementOMutationFn = ApolloReactCommon.MutationFunction; +export type SceneDecrementOComponentProps = Omit, 'mutation'>; + + export const SceneDecrementOComponent = (props: SceneDecrementOComponentProps) => ( + mutation={SceneDecrementODocument} {...props} /> + ); + + +/** + * __useSceneDecrementOMutation__ + * + * To run a mutation, you first call `useSceneDecrementOMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useSceneDecrementOMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [sceneDecrementOMutation, { data, loading, error }] = useSceneDecrementOMutation({ + * variables: { + * id: // value for 'id' + * }, + * }); + */ +export function useSceneDecrementOMutation(baseOptions?: ApolloReactHooks.MutationHookOptions) { + return ApolloReactHooks.useMutation(SceneDecrementODocument, baseOptions); + } +export type SceneDecrementOMutationHookResult = ReturnType; +export type SceneDecrementOMutationResult = ApolloReactCommon.MutationResult; +export type SceneDecrementOMutationOptions = ApolloReactCommon.BaseMutationOptions; +export const SceneResetODocument = gql` + mutation SceneResetO($id: ID!) { + sceneResetO(id: $id) +} + `; +export type SceneResetOMutationFn = ApolloReactCommon.MutationFunction; +export type SceneResetOComponentProps = Omit, 'mutation'>; + + export const SceneResetOComponent = (props: SceneResetOComponentProps) => ( + mutation={SceneResetODocument} {...props} /> + ); + + +/** + * __useSceneResetOMutation__ + * + * To run a mutation, you first call `useSceneResetOMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useSceneResetOMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [sceneResetOMutation, { data, loading, error }] = useSceneResetOMutation({ + * variables: { + * id: // value for 'id' + * }, + * }); + */ +export function useSceneResetOMutation(baseOptions?: ApolloReactHooks.MutationHookOptions) { + return ApolloReactHooks.useMutation(SceneResetODocument, baseOptions); + } +export type SceneResetOMutationHookResult = ReturnType; +export type SceneResetOMutationResult = ApolloReactCommon.MutationResult; +export type SceneResetOMutationOptions = ApolloReactCommon.BaseMutationOptions; export const SceneDestroyDocument = gql` mutation SceneDestroy($id: ID!, $delete_file: Boolean, $delete_generated: Boolean) { sceneDestroy(input: {id: $id, delete_file: $delete_file, delete_generated: $delete_generated}) diff --git a/ui/v2.5/src/models/list-filter/criteria/criterion.ts b/ui/v2.5/src/models/list-filter/criteria/criterion.ts index ee0c9118a..a325e8cd6 100644 --- a/ui/v2.5/src/models/list-filter/criteria/criterion.ts +++ b/ui/v2.5/src/models/list-filter/criteria/criterion.ts @@ -7,6 +7,7 @@ import { ILabeledId, ILabeledValue } from "../types"; export type CriterionType = | "none" | "rating" + | "o_counter" | "resolution" | "duration" | "favorite" @@ -36,6 +37,8 @@ export abstract class Criterion