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 { Button, ButtonGroup, InputGroup, Form } from "react-bootstrap";
|
||||
import { Icon } from "src/components/Shared";
|
||||
import { TextUtils } from "src/utils";
|
||||
import { DurationUtils } from "src/utils";
|
||||
|
||||
interface IProps {
|
||||
disabled?: boolean;
|
||||
@@ -11,67 +11,21 @@ interface IProps {
|
||||
}
|
||||
|
||||
export const DurationInput: React.FC<IProps> = (props: IProps) => {
|
||||
const [value, setValue] = useState<string>(
|
||||
secondsToString(props.numericValue)
|
||||
);
|
||||
const [value, setValue] = useState<string>(DurationUtils.secondsToString(props.numericValue));
|
||||
|
||||
useEffect(() => {
|
||||
setValue(secondsToString(props.numericValue));
|
||||
setValue(DurationUtils.secondsToString(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() {
|
||||
let seconds = stringToSeconds(value);
|
||||
let seconds = DurationUtils.stringToSeconds(value);
|
||||
seconds += 1;
|
||||
props.onValueChange(seconds);
|
||||
}
|
||||
|
||||
function decrement() {
|
||||
let seconds = stringToSeconds(value);
|
||||
let seconds = DurationUtils.stringToSeconds(value);
|
||||
seconds -= 1;
|
||||
props.onValueChange(seconds);
|
||||
}
|
||||
@@ -112,7 +66,7 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
|
||||
disabled={props.disabled}
|
||||
value={value}
|
||||
onChange={(e: any) => setValue(e.target.value)}
|
||||
onBlur={() => props.onValueChange(stringToSeconds(value))}
|
||||
onBlur={() => props.onValueChange(DurationUtils.stringToSeconds(value))}
|
||||
placeholder="hh:mm:ss"
|
||||
/>
|
||||
<InputGroup.Append>
|
||||
|
||||
@@ -5,7 +5,8 @@ import { Icon, FilterSelect } from "src/components/Shared";
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import {
|
||||
Criterion,
|
||||
CriterionType
|
||||
CriterionType,
|
||||
DurationCriterion
|
||||
} from "src/models/list-filter/criteria/criterion";
|
||||
import { NoneCriterion } from "src/models/list-filter/criteria/none";
|
||||
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 { makeCriteria } from "src/models/list-filter/criteria/utils";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { DurationInput } from "src/components/Shared";
|
||||
|
||||
interface IAddFilterProps {
|
||||
onAddCriterion: (criterion: Criterion, oldId?: string) => void;
|
||||
@@ -66,6 +68,11 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
||||
valueStage.current = event.target.value;
|
||||
}
|
||||
|
||||
function onChangedDuration(valueAsNumber: number) {
|
||||
valueStage.current = valueAsNumber;
|
||||
onBlurInput();
|
||||
}
|
||||
|
||||
function onBlurInput() {
|
||||
const newCriterion = _.cloneDeep(criterion);
|
||||
newCriterion.value = valueStage.current;
|
||||
@@ -170,6 +177,15 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
||||
</Form.Control>
|
||||
);
|
||||
}
|
||||
if (criterion instanceof DurationCriterion) {
|
||||
// render duration control
|
||||
return (
|
||||
<DurationInput
|
||||
numericValue={criterion.value ? criterion.value : 0}
|
||||
onValueChange={onChangedDuration}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Form.Control
|
||||
type={criterion.inputType}
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import { ILabeledId, ILabeledValue } from "../types";
|
||||
import { DurationUtils } from 'src/utils';
|
||||
|
||||
export type CriterionType =
|
||||
| "none"
|
||||
| "rating"
|
||||
| "resolution"
|
||||
| "duration"
|
||||
| "favorite"
|
||||
| "hasMarkers"
|
||||
| "isMissing"
|
||||
@@ -36,6 +38,7 @@ export abstract class Criterion<Option = any, Value = any> {
|
||||
return "Rating";
|
||||
case "resolution":
|
||||
return "Resolution";
|
||||
case "duration": return "Duration";
|
||||
case "favorite":
|
||||
return "Favorite";
|
||||
case "hasMarkers":
|
||||
@@ -144,13 +147,18 @@ export abstract class Criterion<Option = any, Value = any> {
|
||||
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;
|
||||
if (
|
||||
this.modifier === CriterionModifier.IsNull ||
|
||||
this.modifier === CriterionModifier.NotNull
|
||||
) {
|
||||
valueString = "";
|
||||
} else if (Array.isArray(this.value) && this.value.length > 0) {
|
||||
if (Array.isArray(this.value) && this.value.length > 0) {
|
||||
let items = this.value;
|
||||
if ((this.value as ILabeledId[])[0].label) {
|
||||
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();
|
||||
}
|
||||
|
||||
return `${Criterion.getLabel(this.type)} ${modifierString} ${valueString}`;
|
||||
return valueString;
|
||||
}
|
||||
|
||||
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,
|
||||
CriterionType,
|
||||
StringCriterion,
|
||||
NumberCriterion
|
||||
NumberCriterion,
|
||||
DurationCriterion
|
||||
} from "./criterion";
|
||||
import { FavoriteCriterion } from "./favorite";
|
||||
import { HasMarkersCriterion } from "./has-markers";
|
||||
@@ -24,6 +25,7 @@ export function makeCriteria(type: CriterionType = "none") {
|
||||
return new RatingCriterion();
|
||||
case "resolution":
|
||||
return new ResolutionCriterion();
|
||||
case "duration": return new DurationCriterion(type, type);
|
||||
case "favorite":
|
||||
return new FavoriteCriterion();
|
||||
case "hasMarkers":
|
||||
@@ -39,7 +41,7 @@ export function makeCriteria(type: CriterionType = "none") {
|
||||
case "studios":
|
||||
return new StudiosCriterion();
|
||||
|
||||
case "birth_year":
|
||||
case "birth_year": return new NumberCriterion(type, type);
|
||||
case "age": {
|
||||
const ret = new NumberCriterion(type, type);
|
||||
// null/not null doesn't make sense for these criteria
|
||||
|
||||
@@ -13,7 +13,8 @@ import {
|
||||
CriterionType,
|
||||
CriterionOption,
|
||||
NumberCriterion,
|
||||
StringCriterion
|
||||
StringCriterion,
|
||||
DurationCriterion
|
||||
} from "./criteria/criterion";
|
||||
import {
|
||||
FavoriteCriterion,
|
||||
@@ -69,6 +70,10 @@ export class ListFilterModel {
|
||||
public criterionOptions: ICriterionOption[] = [];
|
||||
public criteria: Array<Criterion<any, any>> = [];
|
||||
|
||||
private static createCriterionOption(criterion: CriterionType) {
|
||||
return new CriterionOption(Criterion.getLabel(criterion), criterion);
|
||||
}
|
||||
|
||||
public constructor(filterMode: FilterMode, rawParms?: any) {
|
||||
switch (filterMode) {
|
||||
case FilterMode.Scenes:
|
||||
@@ -95,6 +100,7 @@ export class ListFilterModel {
|
||||
new NoneCriterionOption(),
|
||||
new RatingCriterionOption(),
|
||||
new ResolutionCriterionOption(),
|
||||
ListFilterModel.createCriterionOption("duration"),
|
||||
new HasMarkersCriterionOption(),
|
||||
new IsMissingCriterionOption(),
|
||||
new TagsCriterionOption(),
|
||||
@@ -130,7 +136,7 @@ export class ListFilterModel {
|
||||
|
||||
this.criterionOptions = this.criterionOptions.concat(
|
||||
numberCriteria.concat(stringCriteria).map(c => {
|
||||
return new CriterionOption(Criterion.getLabel(c), c);
|
||||
return ListFilterModel.createCriterionOption(c);
|
||||
})
|
||||
);
|
||||
break;
|
||||
@@ -291,6 +297,11 @@ export class ListFilterModel {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "duration": {
|
||||
const durationCrit = criterion as DurationCriterion;
|
||||
result.duration = { value: durationCrit.value, modifier: durationCrit.modifier }
|
||||
break;
|
||||
}
|
||||
case "hasMarkers":
|
||||
result.has_markers = (criterion as HasMarkersCriterion).value;
|
||||
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 TableUtils } from "./table";
|
||||
export { default as TextUtils } from "./text";
|
||||
export { default as DurationUtils } from './duration';
|
||||
|
||||
Reference in New Issue
Block a user