mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Add scene duration filter (#313)
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Button, ButtonGroup, InputGroup, Form } from "react-bootstrap";
|
import { Button, ButtonGroup, InputGroup, Form } from "react-bootstrap";
|
||||||
import { Icon } from "src/components/Shared";
|
import { Icon } from "src/components/Shared";
|
||||||
import { TextUtils } from "src/utils";
|
import { DurationUtils } from "src/utils";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@@ -11,67 +11,21 @@ interface IProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const DurationInput: React.FC<IProps> = (props: IProps) => {
|
export const DurationInput: React.FC<IProps> = (props: IProps) => {
|
||||||
const [value, setValue] = useState<string>(
|
const [value, setValue] = useState<string>(DurationUtils.secondsToString(props.numericValue));
|
||||||
secondsToString(props.numericValue)
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setValue(secondsToString(props.numericValue));
|
setValue(DurationUtils.secondsToString(props.numericValue));
|
||||||
}, [props.numericValue]);
|
}, [props.numericValue]);
|
||||||
|
|
||||||
function secondsToString(seconds: number) {
|
|
||||||
let ret = TextUtils.secondsToTimestamp(seconds);
|
|
||||||
|
|
||||||
if (ret.startsWith("00:")) {
|
|
||||||
ret = ret.substr(3);
|
|
||||||
|
|
||||||
if (ret.startsWith("0")) {
|
|
||||||
ret = ret.substr(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
function stringToSeconds(v: string) {
|
|
||||||
if (!v) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const splits = v.split(":");
|
|
||||||
|
|
||||||
if (splits.length > 3) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let seconds = 0;
|
|
||||||
let factor = 1;
|
|
||||||
while (splits.length > 0) {
|
|
||||||
const thisSplit = splits.pop();
|
|
||||||
if (thisSplit === undefined) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const thisInt = parseInt(thisSplit, 10);
|
|
||||||
if (Number.isNaN(thisInt)) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
seconds += factor * thisInt;
|
|
||||||
factor *= 60;
|
|
||||||
}
|
|
||||||
|
|
||||||
return seconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
function increment() {
|
function increment() {
|
||||||
let seconds = stringToSeconds(value);
|
let seconds = DurationUtils.stringToSeconds(value);
|
||||||
seconds += 1;
|
seconds += 1;
|
||||||
props.onValueChange(seconds);
|
props.onValueChange(seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
function decrement() {
|
function decrement() {
|
||||||
let seconds = stringToSeconds(value);
|
let seconds = DurationUtils.stringToSeconds(value);
|
||||||
seconds -= 1;
|
seconds -= 1;
|
||||||
props.onValueChange(seconds);
|
props.onValueChange(seconds);
|
||||||
}
|
}
|
||||||
@@ -112,7 +66,7 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
|
|||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e: any) => setValue(e.target.value)}
|
onChange={(e: any) => setValue(e.target.value)}
|
||||||
onBlur={() => props.onValueChange(stringToSeconds(value))}
|
onBlur={() => props.onValueChange(DurationUtils.stringToSeconds(value))}
|
||||||
placeholder="hh:mm:ss"
|
placeholder="hh:mm:ss"
|
||||||
/>
|
/>
|
||||||
<InputGroup.Append>
|
<InputGroup.Append>
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { Icon, FilterSelect } from "src/components/Shared";
|
|||||||
import { CriterionModifier } from "src/core/generated-graphql";
|
import { CriterionModifier } from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
Criterion,
|
Criterion,
|
||||||
CriterionType
|
CriterionType,
|
||||||
|
DurationCriterion
|
||||||
} from "src/models/list-filter/criteria/criterion";
|
} from "src/models/list-filter/criteria/criterion";
|
||||||
import { NoneCriterion } from "src/models/list-filter/criteria/none";
|
import { NoneCriterion } from "src/models/list-filter/criteria/none";
|
||||||
import { PerformersCriterion } from "src/models/list-filter/criteria/performers";
|
import { PerformersCriterion } from "src/models/list-filter/criteria/performers";
|
||||||
@@ -13,6 +14,7 @@ import { StudiosCriterion } from "src/models/list-filter/criteria/studios";
|
|||||||
import { TagsCriterion } from "src/models/list-filter/criteria/tags";
|
import { TagsCriterion } from "src/models/list-filter/criteria/tags";
|
||||||
import { makeCriteria } from "src/models/list-filter/criteria/utils";
|
import { makeCriteria } from "src/models/list-filter/criteria/utils";
|
||||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
|
import { DurationInput } from "src/components/Shared";
|
||||||
|
|
||||||
interface IAddFilterProps {
|
interface IAddFilterProps {
|
||||||
onAddCriterion: (criterion: Criterion, oldId?: string) => void;
|
onAddCriterion: (criterion: Criterion, oldId?: string) => void;
|
||||||
@@ -66,6 +68,11 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
|||||||
valueStage.current = event.target.value;
|
valueStage.current = event.target.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onChangedDuration(valueAsNumber: number) {
|
||||||
|
valueStage.current = valueAsNumber;
|
||||||
|
onBlurInput();
|
||||||
|
}
|
||||||
|
|
||||||
function onBlurInput() {
|
function onBlurInput() {
|
||||||
const newCriterion = _.cloneDeep(criterion);
|
const newCriterion = _.cloneDeep(criterion);
|
||||||
newCriterion.value = valueStage.current;
|
newCriterion.value = valueStage.current;
|
||||||
@@ -170,6 +177,15 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
|||||||
</Form.Control>
|
</Form.Control>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (criterion instanceof DurationCriterion) {
|
||||||
|
// render duration control
|
||||||
|
return (
|
||||||
|
<DurationInput
|
||||||
|
numericValue={criterion.value ? criterion.value : 0}
|
||||||
|
onValueChange={onChangedDuration}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Form.Control
|
<Form.Control
|
||||||
type={criterion.inputType}
|
type={criterion.inputType}
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
import { CriterionModifier } from "src/core/generated-graphql";
|
import { CriterionModifier } from "src/core/generated-graphql";
|
||||||
import { ILabeledId, ILabeledValue } from "../types";
|
import { ILabeledId, ILabeledValue } from "../types";
|
||||||
|
import { DurationUtils } from 'src/utils';
|
||||||
|
|
||||||
export type CriterionType =
|
export type CriterionType =
|
||||||
| "none"
|
| "none"
|
||||||
| "rating"
|
| "rating"
|
||||||
| "resolution"
|
| "resolution"
|
||||||
|
| "duration"
|
||||||
| "favorite"
|
| "favorite"
|
||||||
| "hasMarkers"
|
| "hasMarkers"
|
||||||
| "isMissing"
|
| "isMissing"
|
||||||
@@ -36,6 +38,7 @@ export abstract class Criterion<Option = any, Value = any> {
|
|||||||
return "Rating";
|
return "Rating";
|
||||||
case "resolution":
|
case "resolution":
|
||||||
return "Resolution";
|
return "Resolution";
|
||||||
|
case "duration": return "Duration";
|
||||||
case "favorite":
|
case "favorite":
|
||||||
return "Favorite";
|
return "Favorite";
|
||||||
case "hasMarkers":
|
case "hasMarkers":
|
||||||
@@ -144,13 +147,18 @@ export abstract class Criterion<Option = any, Value = any> {
|
|||||||
modifierString = "";
|
modifierString = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let valueString = "";
|
||||||
|
|
||||||
|
if (this.modifier !== CriterionModifier.IsNull && this.modifier !== CriterionModifier.NotNull) {
|
||||||
|
valueString = this.getLabelValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${Criterion.getLabel(this.type)} ${modifierString} ${valueString}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getLabelValue() {
|
||||||
let valueString: string;
|
let valueString: string;
|
||||||
if (
|
if (Array.isArray(this.value) && this.value.length > 0) {
|
||||||
this.modifier === CriterionModifier.IsNull ||
|
|
||||||
this.modifier === CriterionModifier.NotNull
|
|
||||||
) {
|
|
||||||
valueString = "";
|
|
||||||
} else if (Array.isArray(this.value) && this.value.length > 0) {
|
|
||||||
let items = this.value;
|
let items = this.value;
|
||||||
if ((this.value as ILabeledId[])[0].label) {
|
if ((this.value as ILabeledId[])[0].label) {
|
||||||
items = this.value.map(item => item.label) as any;
|
items = this.value.map(item => item.label) as any;
|
||||||
@@ -162,7 +170,7 @@ export abstract class Criterion<Option = any, Value = any> {
|
|||||||
valueString = (this.value as any).toString();
|
valueString = (this.value as any).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${Criterion.getLabel(this.type)} ${modifierString} ${valueString}`;
|
return valueString;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getId(): string {
|
public getId(): string {
|
||||||
@@ -251,3 +259,34 @@ export class NumberCriterion extends Criterion<number, number> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class DurationCriterion extends Criterion<number, number> {
|
||||||
|
public type: CriterionType;
|
||||||
|
public parameterName: string;
|
||||||
|
public modifier = CriterionModifier.Equals;
|
||||||
|
public modifierOptions = [
|
||||||
|
Criterion.getModifierOption(CriterionModifier.Equals),
|
||||||
|
Criterion.getModifierOption(CriterionModifier.NotEquals),
|
||||||
|
Criterion.getModifierOption(CriterionModifier.GreaterThan),
|
||||||
|
Criterion.getModifierOption(CriterionModifier.LessThan),
|
||||||
|
];
|
||||||
|
public options: number[] | undefined;
|
||||||
|
public value: number = 0;
|
||||||
|
|
||||||
|
constructor(type : CriterionType, parameterName?: string, options? : number[]) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.type = type;
|
||||||
|
this.options = options;
|
||||||
|
|
||||||
|
if (!!parameterName) {
|
||||||
|
this.parameterName = parameterName;
|
||||||
|
} else {
|
||||||
|
this.parameterName = type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getLabelValue() {
|
||||||
|
return DurationUtils.secondsToString(this.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import {
|
|||||||
Criterion,
|
Criterion,
|
||||||
CriterionType,
|
CriterionType,
|
||||||
StringCriterion,
|
StringCriterion,
|
||||||
NumberCriterion
|
NumberCriterion,
|
||||||
|
DurationCriterion
|
||||||
} from "./criterion";
|
} from "./criterion";
|
||||||
import { FavoriteCriterion } from "./favorite";
|
import { FavoriteCriterion } from "./favorite";
|
||||||
import { HasMarkersCriterion } from "./has-markers";
|
import { HasMarkersCriterion } from "./has-markers";
|
||||||
@@ -24,6 +25,7 @@ export function makeCriteria(type: CriterionType = "none") {
|
|||||||
return new RatingCriterion();
|
return new RatingCriterion();
|
||||||
case "resolution":
|
case "resolution":
|
||||||
return new ResolutionCriterion();
|
return new ResolutionCriterion();
|
||||||
|
case "duration": return new DurationCriterion(type, type);
|
||||||
case "favorite":
|
case "favorite":
|
||||||
return new FavoriteCriterion();
|
return new FavoriteCriterion();
|
||||||
case "hasMarkers":
|
case "hasMarkers":
|
||||||
@@ -39,7 +41,7 @@ export function makeCriteria(type: CriterionType = "none") {
|
|||||||
case "studios":
|
case "studios":
|
||||||
return new StudiosCriterion();
|
return new StudiosCriterion();
|
||||||
|
|
||||||
case "birth_year":
|
case "birth_year": return new NumberCriterion(type, type);
|
||||||
case "age": {
|
case "age": {
|
||||||
const ret = new NumberCriterion(type, type);
|
const ret = new NumberCriterion(type, type);
|
||||||
// null/not null doesn't make sense for these criteria
|
// null/not null doesn't make sense for these criteria
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import {
|
|||||||
CriterionType,
|
CriterionType,
|
||||||
CriterionOption,
|
CriterionOption,
|
||||||
NumberCriterion,
|
NumberCriterion,
|
||||||
StringCriterion
|
StringCriterion,
|
||||||
|
DurationCriterion
|
||||||
} from "./criteria/criterion";
|
} from "./criteria/criterion";
|
||||||
import {
|
import {
|
||||||
FavoriteCriterion,
|
FavoriteCriterion,
|
||||||
@@ -69,6 +70,10 @@ export class ListFilterModel {
|
|||||||
public criterionOptions: ICriterionOption[] = [];
|
public criterionOptions: ICriterionOption[] = [];
|
||||||
public criteria: Array<Criterion<any, any>> = [];
|
public criteria: Array<Criterion<any, any>> = [];
|
||||||
|
|
||||||
|
private static createCriterionOption(criterion: CriterionType) {
|
||||||
|
return new CriterionOption(Criterion.getLabel(criterion), criterion);
|
||||||
|
}
|
||||||
|
|
||||||
public constructor(filterMode: FilterMode, rawParms?: any) {
|
public constructor(filterMode: FilterMode, rawParms?: any) {
|
||||||
switch (filterMode) {
|
switch (filterMode) {
|
||||||
case FilterMode.Scenes:
|
case FilterMode.Scenes:
|
||||||
@@ -95,6 +100,7 @@ export class ListFilterModel {
|
|||||||
new NoneCriterionOption(),
|
new NoneCriterionOption(),
|
||||||
new RatingCriterionOption(),
|
new RatingCriterionOption(),
|
||||||
new ResolutionCriterionOption(),
|
new ResolutionCriterionOption(),
|
||||||
|
ListFilterModel.createCriterionOption("duration"),
|
||||||
new HasMarkersCriterionOption(),
|
new HasMarkersCriterionOption(),
|
||||||
new IsMissingCriterionOption(),
|
new IsMissingCriterionOption(),
|
||||||
new TagsCriterionOption(),
|
new TagsCriterionOption(),
|
||||||
@@ -130,7 +136,7 @@ export class ListFilterModel {
|
|||||||
|
|
||||||
this.criterionOptions = this.criterionOptions.concat(
|
this.criterionOptions = this.criterionOptions.concat(
|
||||||
numberCriteria.concat(stringCriteria).map(c => {
|
numberCriteria.concat(stringCriteria).map(c => {
|
||||||
return new CriterionOption(Criterion.getLabel(c), c);
|
return ListFilterModel.createCriterionOption(c);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@@ -291,6 +297,11 @@ export class ListFilterModel {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "duration": {
|
||||||
|
const durationCrit = criterion as DurationCriterion;
|
||||||
|
result.duration = { value: durationCrit.value, modifier: durationCrit.modifier }
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "hasMarkers":
|
case "hasMarkers":
|
||||||
result.has_markers = (criterion as HasMarkersCriterion).value;
|
result.has_markers = (criterion as HasMarkersCriterion).value;
|
||||||
break;
|
break;
|
||||||
|
|||||||
51
ui/v2.5/src/utils/duration.ts
Normal file
51
ui/v2.5/src/utils/duration.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import TextUtils from "./text";
|
||||||
|
|
||||||
|
const secondsToString = (seconds : number) => {
|
||||||
|
let ret = TextUtils.secondsToTimestamp(seconds);
|
||||||
|
|
||||||
|
if (ret.startsWith("00:")) {
|
||||||
|
ret = ret.substr(3);
|
||||||
|
|
||||||
|
if (ret.startsWith("0")) {
|
||||||
|
ret = ret.substr(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stringToSeconds = (v : string) => {
|
||||||
|
if (!v) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const splits = v.split(":");
|
||||||
|
|
||||||
|
if (splits.length > 3) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let seconds = 0;
|
||||||
|
let factor = 1;
|
||||||
|
while(splits.length > 0) {
|
||||||
|
const thisSplit = splits.pop();
|
||||||
|
if (thisSplit === undefined) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const thisInt = parseInt(thisSplit, 10);
|
||||||
|
if (Number.isNaN(thisInt)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
seconds += factor * thisInt;
|
||||||
|
factor *= 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
return seconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
secondsToString,
|
||||||
|
stringToSeconds
|
||||||
|
};
|
||||||
@@ -2,3 +2,4 @@ export { default as ImageUtils } from "./image";
|
|||||||
export { default as NavUtils } from "./navigation";
|
export { default as NavUtils } from "./navigation";
|
||||||
export { default as TableUtils } from "./table";
|
export { default as TableUtils } from "./table";
|
||||||
export { default as TextUtils } from "./text";
|
export { default as TextUtils } from "./text";
|
||||||
|
export { default as DurationUtils } from './duration';
|
||||||
|
|||||||
Reference in New Issue
Block a user