diff --git a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx
index 620bc0cc8..cfaa82596 100644
--- a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx
+++ b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx
@@ -20,6 +20,7 @@ import {
} from "src/components/Shared";
import { useToast } from "src/hooks";
import { StudioScenesPanel } from "./StudioScenesPanel";
+import { StudioImagesPanel } from "./StudioImagesPanel";
import { StudioChildrenPanel } from "./StudioChildrenPanel";
interface IStudioParams {
@@ -195,7 +196,8 @@ export const Studio: React.FC = () => {
);
}
- const activeTabKey = tab === "childstudios" ? tab : "scenes";
+ const activeTabKey =
+ tab === "childstudios" || tab === "images" ? tab : "scenes";
const setActiveTabKey = (newTab: string | null) => {
if (tab !== newTab) {
const tabParam = newTab === "scenes" ? "" : `/${newTab}`;
@@ -290,6 +292,9 @@ export const Studio: React.FC = () => {
+
+
+
diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioImagesPanel.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioImagesPanel.tsx
new file mode 100644
index 000000000..429e39c5e
--- /dev/null
+++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioImagesPanel.tsx
@@ -0,0 +1,12 @@
+import React from "react";
+import * as GQL from "src/core/generated-graphql";
+import { studioFilterHook } from "src/core/studios";
+import { ImageList } from "src/components/Images/ImageList";
+
+interface IStudioImagesPanel {
+ studio: Partial;
+}
+
+export const StudioImagesPanel: React.FC = ({ studio }) => {
+ return ;
+};
diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioScenesPanel.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioScenesPanel.tsx
index 952c0d33a..aefd7614b 100644
--- a/ui/v2.5/src/components/Studios/StudioDetails/StudioScenesPanel.tsx
+++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioScenesPanel.tsx
@@ -1,45 +1,12 @@
import React from "react";
import * as GQL from "src/core/generated-graphql";
-import { StudiosCriterion } from "src/models/list-filter/criteria/studios";
-import { ListFilterModel } from "src/models/list-filter/filter";
import { SceneList } from "src/components/Scenes/SceneList";
+import { studioFilterHook } from "src/core/studios";
interface IStudioScenesPanel {
studio: Partial;
}
export const StudioScenesPanel: React.FC = ({ studio }) => {
- function filterHook(filter: ListFilterModel) {
- const studioValue = { id: studio.id!, label: studio.name! };
- // if studio is already present, then we modify it, otherwise add
- let studioCriterion = filter.criteria.find((c) => {
- return c.type === "studios";
- }) as StudiosCriterion;
-
- if (
- studioCriterion &&
- (studioCriterion.modifier === GQL.CriterionModifier.IncludesAll ||
- studioCriterion.modifier === GQL.CriterionModifier.Includes)
- ) {
- // add the studio if not present
- if (
- !studioCriterion.value.find((p) => {
- return p.id === studio.id;
- })
- ) {
- studioCriterion.value.push(studioValue);
- }
-
- studioCriterion.modifier = GQL.CriterionModifier.IncludesAll;
- } else {
- // overwrite
- studioCriterion = new StudiosCriterion();
- studioCriterion.value = [studioValue];
- filter.criteria.push(studioCriterion);
- }
-
- return filter;
- }
-
- return ;
+ return ;
};
diff --git a/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx b/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx
index b32712751..a01f147a9 100644
--- a/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx
+++ b/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx
@@ -20,6 +20,7 @@ import {
import { useToast } from "src/hooks";
import { TagScenesPanel } from "./TagScenesPanel";
import { TagMarkersPanel } from "./TagMarkersPanel";
+import { TagImagesPanel } from "./TagImagesPanel";
interface ITabParams {
id?: string;
@@ -49,7 +50,7 @@ export const Tag: React.FC = () => {
const [createTag] = useTagCreate(getTagInput() as GQL.TagUpdateInput);
const [deleteTag] = useTagDestroy(getTagInput() as GQL.TagUpdateInput);
- const activeTabKey = tab === "markers" ? tab : "scenes";
+ const activeTabKey = tab === "markers" || tab === "images" ? tab : "scenes";
const setActiveTabKey = (newTab: string | null) => {
if (tab !== newTab) {
const tabParam = newTab === "scenes" ? "" : `/${newTab}`;
@@ -246,6 +247,9 @@ export const Tag: React.FC = () => {
+
+
+
diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagImagesPanel.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagImagesPanel.tsx
new file mode 100644
index 000000000..5df08f505
--- /dev/null
+++ b/ui/v2.5/src/components/Tags/TagDetails/TagImagesPanel.tsx
@@ -0,0 +1,12 @@
+import React from "react";
+import * as GQL from "src/core/generated-graphql";
+import { tagFilterHook } from "src/core/tags";
+import { ImageList } from "src/components/Images/ImageList";
+
+interface ITagImagesPanel {
+ tag: GQL.TagDataFragment;
+}
+
+export const TagImagesPanel: React.FC = ({ tag }) => {
+ return ;
+};
diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagScenesPanel.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagScenesPanel.tsx
index 1c402dae7..1cfd0dc68 100644
--- a/ui/v2.5/src/components/Tags/TagDetails/TagScenesPanel.tsx
+++ b/ui/v2.5/src/components/Tags/TagDetails/TagScenesPanel.tsx
@@ -1,45 +1,12 @@
import React from "react";
import * as GQL from "src/core/generated-graphql";
-import { ListFilterModel } from "src/models/list-filter/filter";
import { SceneList } from "src/components/Scenes/SceneList";
-import { TagsCriterion } from "src/models/list-filter/criteria/tags";
+import { tagFilterHook } from "src/core/tags";
interface ITagScenesPanel {
tag: GQL.TagDataFragment;
}
export const TagScenesPanel: React.FC = ({ tag }) => {
- function filterHook(filter: ListFilterModel) {
- const tagValue = { id: tag.id, label: tag.name };
- // if tag is already present, then we modify it, otherwise add
- let tagCriterion = filter.criteria.find((c) => {
- return c.type === "tags";
- }) as TagsCriterion;
-
- if (
- tagCriterion &&
- (tagCriterion.modifier === GQL.CriterionModifier.IncludesAll ||
- tagCriterion.modifier === GQL.CriterionModifier.Includes)
- ) {
- // add the tag if not present
- if (
- !tagCriterion.value.find((p) => {
- return p.id === tag.id;
- })
- ) {
- tagCriterion.value.push(tagValue);
- }
-
- tagCriterion.modifier = GQL.CriterionModifier.IncludesAll;
- } else {
- // overwrite
- tagCriterion = new TagsCriterion("tags");
- tagCriterion.value = [tagValue];
- filter.criteria.push(tagCriterion);
- }
-
- return filter;
- }
-
- return ;
+ return ;
};
diff --git a/ui/v2.5/src/core/performers.ts b/ui/v2.5/src/core/performers.ts
new file mode 100644
index 000000000..e7552b447
--- /dev/null
+++ b/ui/v2.5/src/core/performers.ts
@@ -0,0 +1,39 @@
+import { PerformersCriterion } from "src/models/list-filter/criteria/performers";
+import * as GQL from "src/core/generated-graphql";
+import { ListFilterModel } from "src/models/list-filter/filter";
+
+export const performerFilterHook = (
+ performer: Partial
+) => {
+ return (filter: ListFilterModel) => {
+ const performerValue = { id: performer.id!, label: performer.name! };
+ // if performers is already present, then we modify it, otherwise add
+ let performerCriterion = filter.criteria.find((c) => {
+ return c.type === "performers";
+ }) as PerformersCriterion;
+
+ if (
+ performerCriterion &&
+ (performerCriterion.modifier === GQL.CriterionModifier.IncludesAll ||
+ performerCriterion.modifier === GQL.CriterionModifier.Includes)
+ ) {
+ // add the performer if not present
+ if (
+ !performerCriterion.value.find((p) => {
+ return p.id === performer.id;
+ })
+ ) {
+ performerCriterion.value.push(performerValue);
+ }
+
+ performerCriterion.modifier = GQL.CriterionModifier.IncludesAll;
+ } else {
+ // overwrite
+ performerCriterion = new PerformersCriterion();
+ performerCriterion.value = [performerValue];
+ filter.criteria.push(performerCriterion);
+ }
+
+ return filter;
+ };
+};
diff --git a/ui/v2.5/src/core/studios.ts b/ui/v2.5/src/core/studios.ts
new file mode 100644
index 000000000..00724b6c5
--- /dev/null
+++ b/ui/v2.5/src/core/studios.ts
@@ -0,0 +1,37 @@
+import * as GQL from "src/core/generated-graphql";
+import { StudiosCriterion } from "src/models/list-filter/criteria/studios";
+import { ListFilterModel } from "src/models/list-filter/filter";
+
+export const studioFilterHook = (studio: Partial) => {
+ return (filter: ListFilterModel) => {
+ const studioValue = { id: studio.id!, label: studio.name! };
+ // if studio is already present, then we modify it, otherwise add
+ let studioCriterion = filter.criteria.find((c) => {
+ return c.type === "studios";
+ }) as StudiosCriterion;
+
+ if (
+ studioCriterion &&
+ (studioCriterion.modifier === GQL.CriterionModifier.IncludesAll ||
+ studioCriterion.modifier === GQL.CriterionModifier.Includes)
+ ) {
+ // add the studio if not present
+ if (
+ !studioCriterion.value.find((p) => {
+ return p.id === studio.id;
+ })
+ ) {
+ studioCriterion.value.push(studioValue);
+ }
+
+ studioCriterion.modifier = GQL.CriterionModifier.IncludesAll;
+ } else {
+ // overwrite
+ studioCriterion = new StudiosCriterion();
+ studioCriterion.value = [studioValue];
+ filter.criteria.push(studioCriterion);
+ }
+
+ return filter;
+ };
+};
diff --git a/ui/v2.5/src/core/tags.ts b/ui/v2.5/src/core/tags.ts
new file mode 100644
index 000000000..d8269f229
--- /dev/null
+++ b/ui/v2.5/src/core/tags.ts
@@ -0,0 +1,37 @@
+import * as GQL from "src/core/generated-graphql";
+import { TagsCriterion } from "src/models/list-filter/criteria/tags";
+import { ListFilterModel } from "src/models/list-filter/filter";
+
+export const tagFilterHook = (tag: GQL.TagDataFragment) => {
+ return (filter: ListFilterModel) => {
+ const tagValue = { id: tag.id, label: tag.name };
+ // if tag is already present, then we modify it, otherwise add
+ let tagCriterion = filter.criteria.find((c) => {
+ return c.type === "tags";
+ }) as TagsCriterion;
+
+ if (
+ tagCriterion &&
+ (tagCriterion.modifier === GQL.CriterionModifier.IncludesAll ||
+ tagCriterion.modifier === GQL.CriterionModifier.Includes)
+ ) {
+ // add the tag if not present
+ if (
+ !tagCriterion.value.find((p) => {
+ return p.id === tag.id;
+ })
+ ) {
+ tagCriterion.value.push(tagValue);
+ }
+
+ tagCriterion.modifier = GQL.CriterionModifier.IncludesAll;
+ } else {
+ // overwrite
+ tagCriterion = new TagsCriterion("tags");
+ tagCriterion.value = [tagValue];
+ filter.criteria.push(tagCriterion);
+ }
+
+ return filter;
+ };
+};
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 22c5f442f..a4e805e71 100644
--- a/ui/v2.5/src/models/list-filter/criteria/criterion.ts
+++ b/ui/v2.5/src/models/list-filter/criteria/criterion.ts
@@ -10,6 +10,7 @@ export type CriterionType =
| "rating"
| "o_counter"
| "resolution"
+ | "average_resolution"
| "duration"
| "favorite"
| "hasMarkers"
@@ -59,6 +60,8 @@ export abstract class Criterion {
return "O-Counter";
case "resolution":
return "Resolution";
+ case "average_resolution":
+ return "Average Resolution";
case "duration":
return "Duration";
case "favorite":
diff --git a/ui/v2.5/src/models/list-filter/criteria/is-missing.ts b/ui/v2.5/src/models/list-filter/criteria/is-missing.ts
index d514ac3a8..caa24bda3 100644
--- a/ui/v2.5/src/models/list-filter/criteria/is-missing.ts
+++ b/ui/v2.5/src/models/list-filter/criteria/is-missing.ts
@@ -73,7 +73,16 @@ export class PerformerIsMissingCriterionOption implements ICriterionOption {
export class GalleryIsMissingCriterion extends IsMissingCriterion {
public type: CriterionType = "galleryIsMissing";
- public options: string[] = ["scene"];
+ public options: string[] = [
+ "title",
+ "details",
+ "url",
+ "date",
+ "studio",
+ "performers",
+ "tags",
+ "scene",
+ ];
}
export class GalleryIsMissingCriterionOption implements ICriterionOption {
diff --git a/ui/v2.5/src/models/list-filter/criteria/resolution.ts b/ui/v2.5/src/models/list-filter/criteria/resolution.ts
index 475fbdc6a..edcd03128 100644
--- a/ui/v2.5/src/models/list-filter/criteria/resolution.ts
+++ b/ui/v2.5/src/models/list-filter/criteria/resolution.ts
@@ -14,3 +14,13 @@ export class ResolutionCriterionOption implements ICriterionOption {
public label: string = Criterion.getLabel("resolution");
public value: CriterionType = "resolution";
}
+
+export class AverageResolutionCriterion extends ResolutionCriterion {
+ public type: CriterionType = "average_resolution";
+ public parameterName: string = "average_resolution";
+}
+
+export class AverageResolutionCriterionOption extends ResolutionCriterionOption {
+ public label: string = Criterion.getLabel("average_resolution");
+ public value: CriterionType = "average_resolution";
+}
diff --git a/ui/v2.5/src/models/list-filter/criteria/utils.ts b/ui/v2.5/src/models/list-filter/criteria/utils.ts
index 36e35545a..0e3d95a47 100644
--- a/ui/v2.5/src/models/list-filter/criteria/utils.ts
+++ b/ui/v2.5/src/models/list-filter/criteria/utils.ts
@@ -22,7 +22,7 @@ import {
import { NoneCriterion } from "./none";
import { PerformersCriterion } from "./performers";
import { RatingCriterion } from "./rating";
-import { ResolutionCriterion } from "./resolution";
+import { AverageResolutionCriterion, ResolutionCriterion } from "./resolution";
import { StudiosCriterion, ParentStudiosCriterion } from "./studios";
import { TagsCriterion } from "./tags";
import { GenderCriterion } from "./gender";
@@ -43,6 +43,8 @@ export function makeCriteria(type: CriterionType = "none") {
return new NumberCriterion(type, type);
case "resolution":
return new ResolutionCriterion();
+ case "average_resolution":
+ return new AverageResolutionCriterion();
case "duration":
return new DurationCriterion(type, type);
case "favorite":
diff --git a/ui/v2.5/src/models/list-filter/filter.ts b/ui/v2.5/src/models/list-filter/filter.ts
index f1c786bff..a53b8e1a6 100644
--- a/ui/v2.5/src/models/list-filter/filter.ts
+++ b/ui/v2.5/src/models/list-filter/filter.ts
@@ -48,6 +48,8 @@ import {
} from "./criteria/performers";
import { RatingCriterion, RatingCriterionOption } from "./criteria/rating";
import {
+ AverageResolutionCriterion,
+ AverageResolutionCriterionOption,
ResolutionCriterion,
ResolutionCriterionOption,
} from "./criteria/resolution";
@@ -154,6 +156,7 @@ export class ListFilterModel {
this.displayModeOptions = [DisplayMode.Grid, DisplayMode.Wall];
this.criterionOptions = [
new NoneCriterionOption(),
+ ListFilterModel.createCriterionOption("path"),
new RatingCriterionOption(),
ListFilterModel.createCriterionOption("o_counter"),
new ResolutionCriterionOption(),
@@ -227,7 +230,12 @@ export class ListFilterModel {
this.criterionOptions = [
new NoneCriterionOption(),
ListFilterModel.createCriterionOption("path"),
+ new RatingCriterionOption(),
+ new AverageResolutionCriterionOption(),
new GalleryIsMissingCriterionOption(),
+ new TagsCriterionOption(),
+ new PerformersCriterionOption(),
+ new StudiosCriterionOption(),
];
break;
case FilterMode.SceneMarkers:
@@ -645,6 +653,14 @@ export class ListFilterModel {
const result: ImageFilterType = {};
this.criteria.forEach((criterion) => {
switch (criterion.type) {
+ case "path": {
+ const pathCrit = criterion as MandatoryStringCriterion;
+ result.path = {
+ value: pathCrit.value,
+ modifier: pathCrit.modifier,
+ };
+ break;
+ }
case "rating": {
const ratingCrit = criterion as RatingCriterion;
result.rating = {
@@ -695,7 +711,7 @@ export class ListFilterModel {
}
case "performers": {
const perfCrit = criterion as PerformersCriterion;
- result.galleries = {
+ result.performers = {
value: perfCrit.value.map((perf) => perf.id),
modifier: perfCrit.modifier,
};
@@ -776,9 +792,62 @@ export class ListFilterModel {
};
break;
}
+ case "rating": {
+ const ratingCrit = criterion as RatingCriterion;
+ result.rating = {
+ value: ratingCrit.value,
+ modifier: ratingCrit.modifier,
+ };
+ break;
+ }
+ case "average_resolution": {
+ switch ((criterion as AverageResolutionCriterion).value) {
+ case "240p":
+ result.average_resolution = ResolutionEnum.Low;
+ break;
+ case "480p":
+ result.average_resolution = ResolutionEnum.Standard;
+ break;
+ case "720p":
+ result.average_resolution = ResolutionEnum.StandardHd;
+ break;
+ case "1080p":
+ result.average_resolution = ResolutionEnum.FullHd;
+ break;
+ case "4k":
+ result.average_resolution = ResolutionEnum.FourK;
+ break;
+ // no default
+ }
+ break;
+ }
case "galleryIsMissing":
result.is_missing = (criterion as IsMissingCriterion).value;
break;
+ case "tags": {
+ const tagsCrit = criterion as TagsCriterion;
+ result.tags = {
+ value: tagsCrit.value.map((tag) => tag.id),
+ modifier: tagsCrit.modifier,
+ };
+ break;
+ }
+ case "performers": {
+ const perfCrit = criterion as PerformersCriterion;
+ result.performers = {
+ value: perfCrit.value.map((perf) => perf.id),
+ modifier: perfCrit.modifier,
+ };
+ break;
+ }
+ case "studios": {
+ const studCrit = criterion as StudiosCriterion;
+ result.studios = {
+ value: studCrit.value.map((studio) => studio.id),
+ modifier: studCrit.modifier,
+ };
+ break;
+ }
// no default
}
});
diff --git a/ui/v2.5/src/utils/text.ts b/ui/v2.5/src/utils/text.ts
index 1558ca37b..1ea04b60e 100644
--- a/ui/v2.5/src/utils/text.ts
+++ b/ui/v2.5/src/utils/text.ts
@@ -16,6 +16,7 @@ const Units: Unit[] = [
"terabyte",
"petabyte",
];
+const shortUnits = ["B", "KB", "MB", "GB", "TB", "PB"];
const truncate = (
value?: string,
@@ -32,7 +33,7 @@ const fileSize = (bytes: number = 0) => {
let unit = 0;
let count = bytes;
- while (count >= 1024) {
+ while (count >= 1024 && unit + 1 < Units.length) {
count /= 1024;
unit++;
}
@@ -43,6 +44,11 @@ const fileSize = (bytes: number = 0) => {
};
};
+const formatFileSizeUnit = (u: Unit) => {
+ const i = Units.indexOf(u);
+ return shortUnits[i];
+};
+
const secondsToTimestamp = (seconds: number) => {
let ret = new Date(seconds * 1000).toISOString().substr(11, 8);
@@ -141,6 +147,7 @@ const formatDate = (intl: IntlShape, date?: string) => {
const TextUtils = {
truncate,
fileSize,
+ formatFileSizeUnit,
secondsToTimestamp,
fileNameFromPath,
age: getAge,