Add O-counter (#334)

This commit is contained in:
Infinite 2020-02-08 16:54:20 +01:00
parent f23247d9c8
commit e6d9d385a7
11 changed files with 369 additions and 11 deletions

View file

@ -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<ISceneCardProps> = (
);
}
function maybeRenderOCounter() {
if (props.scene.o_counter) {
return (
<div>
<Button className="minimal">
<SweatDrops />
<span>{props.scene.o_counter}</span>
</Button>
</div>
)
}
}
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<ISceneCardProps> = (
{maybeRenderTagPopoverButton()}
{maybeRenderPerformerPopoverButton()}
{maybeRenderSceneMarkerPopoverButton()}
{maybeRenderOCounter()}
</ButtonGroup>
</>
);

View file

@ -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<IOCounterButtonProps> = (props: IOCounterButtonProps) => {
if(props.loading)
return <Spinner animation="border" role="status" />;
const renderButton = () => (
<Button
className="minimal"
onClick={props.onIncrement}
variant="secondary"
>
<SweatDrops />
<span className="ml-2">{props.value}</span>
</Button>
);
if (props.value) {
return (
<HoverPopover
content={
<div>
<div>
<Button
className="minimal"
onClick={props.onDecrement}
variant="secondary"
>
<Icon icon="minus" />
<span>Decrement</span>
</Button>
</div>
<div>
<Button
className="minimal"
onClick={props.onReset}
variant="secondary"
>
<Icon icon="ban" />
<span>Reset</span>
</Button>
</div>
</div>
}
enterDelay={1000}
placement="bottom"
onOpen={props.onMenuOpened}
onClose={props.onMenuClosed}
>
{ renderButton() }
</HoverPopover>
);
}
return renderButton();
}

View file

@ -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<number>(getInitialTimestamp());
const [scene, setScene] = useState<GQL.SceneDataFragment | undefined>();
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 = () => {
<>
<ScenePlayer scene={scene} timestamp={timestamp} autoplay={autoplay} />
<div id="scene-details-container" className="col col-sm-9 m-sm-auto">
<div className="float-right">
<OCounterButton
loading={oLoading}
value={scene.o_counter || 0}
onIncrement={onIncrementClick}
onDecrement={onDecrementClick}
onReset={onResetClick}
/>
</div>
<Tabs id="scene-tabs" mountOnEnter>
<Tab eventKey="scene-details-panel" title="Details">
<SceneDetailPanel scene={scene} />

View file

@ -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<IHoverPopover> = ({
@ -15,7 +17,9 @@ export const HoverPopover: React.FC<IHoverPopover> = ({
content,
children,
className,
placement = "top"
placement = "top",
onOpen,
onClose
}) => {
const [show, setShow] = useState(false);
const triggerRef = useRef<HTMLDivElement>(null);
@ -24,13 +28,19 @@ export const HoverPopover: React.FC<IHoverPopover> = ({
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(
() => () => {

View file

@ -0,0 +1,10 @@
import React from 'react';
export const SweatDrops = () => (
<span>
<svg xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" style={{transform: "rotate(360deg)"}} preserveAspectRatio="xMidYMid meet" viewBox="0 0 36 36">
<path fill="currentColor" d="M22.855.758L7.875 7.024l12.537 9.733c2.633 2.224 6.377 2.937 9.77 1.518c4.826-2.018 7.096-7.576 5.072-12.413C33.232 1.024 27.68-1.261 22.855.758zm-9.962 17.924L2.05 10.284L.137 23.529a7.993 7.993 0 0 0 2.958 7.803a8.001 8.001 0 0 0 9.798-12.65zm15.339 7.015l-8.156-4.69l-.033 9.223c-.088 2 .904 3.98 2.75 5.041a5.462 5.462 0 0 0 7.479-2.051c1.499-2.644.589-6.013-2.04-7.523z" />
<rect x="0" y="0" width="36" height="36" fill="rgba(0, 0, 0, 0)" />
</svg>
</span>
);

View file

@ -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';

View file

@ -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,

View file

@ -7,7 +7,7 @@ import * as ApolloReactHooks from '@apollo/react-hooks';
export type Maybe<T> = T | null;
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
// 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<Array<Scene>>,
sceneDestroy: Scalars['Boolean'],
scenesUpdate?: Maybe<Array<Maybe<Scene>>>,
/** 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<SceneMarker>,
sceneMarkerUpdate?: Maybe<SceneMarker>,
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<Scalars['String']>,
date?: Maybe<Scalars['String']>,
rating?: Maybe<Scalars['Int']>,
o_counter?: Maybe<Scalars['Int']>,
path: Scalars['String'],
file: SceneFileType,
paths: ScenePathsType,
@ -794,6 +816,8 @@ export type SceneFileType = {
export type SceneFilterType = {
/** Filter by rating */
rating?: Maybe<IntCriterionInput>,
/** Filter by o-counter */
o_counter?: Maybe<IntCriterionInput>,
/** Filter by resolution */
resolution?: Maybe<ResolutionEnum>,
/** Filter by duration (in seconds) */
@ -1189,7 +1213,7 @@ export type SceneMarkerDataFragment = (
export type SlimSceneDataFragment = (
{ __typename?: 'Scene' }
& Pick<Scene, 'id' | 'checksum' | 'title' | 'details' | 'url' | 'date' | 'rating' | 'path'>
& Pick<Scene, 'id' | 'checksum' | 'title' | 'details' | 'url' | 'date' | 'rating' | 'o_counter' | 'path'>
& { file: (
{ __typename?: 'SceneFileType' }
& Pick<SceneFileType, 'size' | 'duration' | 'video_codec' | 'audio_codec' | 'width' | 'height' | 'framerate' | 'bitrate'>
@ -1216,7 +1240,7 @@ export type SlimSceneDataFragment = (
export type SceneDataFragment = (
{ __typename?: 'Scene' }
& Pick<Scene, 'id' | 'checksum' | 'title' | 'details' | 'url' | 'date' | 'rating' | 'path' | 'is_streamable'>
& Pick<Scene, 'id' | 'checksum' | 'title' | 'details' | 'url' | 'date' | 'rating' | 'o_counter' | 'path' | 'is_streamable'>
& { file: (
{ __typename?: 'SceneFileType' }
& Pick<SceneFileType, 'size' | 'duration' | 'video_codec' | 'audio_codec' | 'width' | 'height' | 'framerate' | 'bitrate'>
@ -1492,6 +1516,36 @@ export type ScenesUpdateMutation = (
)>>> }
);
export type SceneIncrementOMutationVariables = {
id: Scalars['ID']
};
export type SceneIncrementOMutation = (
{ __typename?: 'Mutation' }
& Pick<Mutation, 'sceneIncrementO'>
);
export type SceneDecrementOMutationVariables = {
id: Scalars['ID']
};
export type SceneDecrementOMutation = (
{ __typename?: 'Mutation' }
& Pick<Mutation, 'sceneDecrementO'>
);
export type SceneResetOMutationVariables = {
id: Scalars['ID']
};
export type SceneResetOMutation = (
{ __typename?: 'Mutation' }
& Pick<Mutation, 'sceneResetO'>
);
export type SceneDestroyMutationVariables = {
id: Scalars['ID'],
delete_file?: Maybe<Scalars['Boolean']>,
@ -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<typeof useScenesUpdateMutation>;
export type ScenesUpdateMutationResult = ApolloReactCommon.MutationResult<ScenesUpdateMutation>;
export type ScenesUpdateMutationOptions = ApolloReactCommon.BaseMutationOptions<ScenesUpdateMutation, ScenesUpdateMutationVariables>;
export const SceneIncrementODocument = gql`
mutation SceneIncrementO($id: ID!) {
sceneIncrementO(id: $id)
}
`;
export type SceneIncrementOMutationFn = ApolloReactCommon.MutationFunction<SceneIncrementOMutation, SceneIncrementOMutationVariables>;
export type SceneIncrementOComponentProps = Omit<ApolloReactComponents.MutationComponentOptions<SceneIncrementOMutation, SceneIncrementOMutationVariables>, 'mutation'>;
export const SceneIncrementOComponent = (props: SceneIncrementOComponentProps) => (
<ApolloReactComponents.Mutation<SceneIncrementOMutation, SceneIncrementOMutationVariables> 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<SceneIncrementOMutation, SceneIncrementOMutationVariables>) {
return ApolloReactHooks.useMutation<SceneIncrementOMutation, SceneIncrementOMutationVariables>(SceneIncrementODocument, baseOptions);
}
export type SceneIncrementOMutationHookResult = ReturnType<typeof useSceneIncrementOMutation>;
export type SceneIncrementOMutationResult = ApolloReactCommon.MutationResult<SceneIncrementOMutation>;
export type SceneIncrementOMutationOptions = ApolloReactCommon.BaseMutationOptions<SceneIncrementOMutation, SceneIncrementOMutationVariables>;
export const SceneDecrementODocument = gql`
mutation SceneDecrementO($id: ID!) {
sceneDecrementO(id: $id)
}
`;
export type SceneDecrementOMutationFn = ApolloReactCommon.MutationFunction<SceneDecrementOMutation, SceneDecrementOMutationVariables>;
export type SceneDecrementOComponentProps = Omit<ApolloReactComponents.MutationComponentOptions<SceneDecrementOMutation, SceneDecrementOMutationVariables>, 'mutation'>;
export const SceneDecrementOComponent = (props: SceneDecrementOComponentProps) => (
<ApolloReactComponents.Mutation<SceneDecrementOMutation, SceneDecrementOMutationVariables> 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<SceneDecrementOMutation, SceneDecrementOMutationVariables>) {
return ApolloReactHooks.useMutation<SceneDecrementOMutation, SceneDecrementOMutationVariables>(SceneDecrementODocument, baseOptions);
}
export type SceneDecrementOMutationHookResult = ReturnType<typeof useSceneDecrementOMutation>;
export type SceneDecrementOMutationResult = ApolloReactCommon.MutationResult<SceneDecrementOMutation>;
export type SceneDecrementOMutationOptions = ApolloReactCommon.BaseMutationOptions<SceneDecrementOMutation, SceneDecrementOMutationVariables>;
export const SceneResetODocument = gql`
mutation SceneResetO($id: ID!) {
sceneResetO(id: $id)
}
`;
export type SceneResetOMutationFn = ApolloReactCommon.MutationFunction<SceneResetOMutation, SceneResetOMutationVariables>;
export type SceneResetOComponentProps = Omit<ApolloReactComponents.MutationComponentOptions<SceneResetOMutation, SceneResetOMutationVariables>, 'mutation'>;
export const SceneResetOComponent = (props: SceneResetOComponentProps) => (
<ApolloReactComponents.Mutation<SceneResetOMutation, SceneResetOMutationVariables> 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<SceneResetOMutation, SceneResetOMutationVariables>) {
return ApolloReactHooks.useMutation<SceneResetOMutation, SceneResetOMutationVariables>(SceneResetODocument, baseOptions);
}
export type SceneResetOMutationHookResult = ReturnType<typeof useSceneResetOMutation>;
export type SceneResetOMutationResult = ApolloReactCommon.MutationResult<SceneResetOMutation>;
export type SceneResetOMutationOptions = ApolloReactCommon.BaseMutationOptions<SceneResetOMutation, SceneResetOMutationVariables>;
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})

View file

@ -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<Option = any, Value = any> {
return "None";
case "rating":
return "Rating";
case "o_counter":
return "O-Counter";
case "resolution":
return "Resolution";
case "duration":

View file

@ -23,6 +23,8 @@ export function makeCriteria(type: CriterionType = "none") {
return new NoneCriterion();
case "rating":
return new RatingCriterion();
case "o_counter":
return new NumberCriterion(type, type);
case "resolution":
return new ResolutionCriterion();
case "duration":

View file

@ -92,6 +92,7 @@ export class ListFilterModel {
"title",
"path",
"rating",
"o_counter",
"date",
"filesize",
"duration",
@ -107,6 +108,7 @@ export class ListFilterModel {
this.criterionOptions = [
new NoneCriterionOption(),
new RatingCriterionOption(),
ListFilterModel.createCriterionOption("o_counter"),
new ResolutionCriterionOption(),
ListFilterModel.createCriterionOption("duration"),
new HasMarkersCriterionOption(),
@ -294,7 +296,7 @@ export class ListFilterModel {
q: this.searchTerm,
page: this.currentPage,
per_page: this.itemsPerPage,
sort: this.getSortBy(),
sort: this.sortBy,
direction: this.sortDirection
};
}
@ -311,6 +313,11 @@ export class ListFilterModel {
};
break;
}
case "o_counter": {
const oCounterCrit = criterion as NumberCriterion;
result.o_counter = { value: oCounterCrit.value, modifier: oCounterCrit.modifier };
break;
}
case "resolution": {
switch ((criterion as ResolutionCriterion).value) {
case "240p":