Image improvements (#847)

* Fix image performer filtering
* Add performer images tab
* Add studio images tab
* Rename interface
* Add tag images tab
* Add path filtering for images
* Show image stats on stats page
* Fix incorrect scan counts after timeout
* Add gallery filters
* Relax scene gallery selector
This commit is contained in:
WithoutPants
2020-10-20 10:11:15 +11:00
committed by GitHub
parent 80199f79f3
commit 8eda72ad89
31 changed files with 463 additions and 169 deletions

View File

@@ -21,6 +21,7 @@ import FsLightbox from "fslightbox-react";
import { PerformerDetailsPanel } from "./PerformerDetailsPanel";
import { PerformerOperationsPanel } from "./PerformerOperationsPanel";
import { PerformerScenesPanel } from "./PerformerScenesPanel";
import { PerformerImagesPanel } from "./PerformerImagesPanel";
interface IPerformerParams {
id?: string;
@@ -57,7 +58,10 @@ export const Performer: React.FC = () => {
const [deletePerformer] = usePerformerDestroy();
const activeTabKey =
tab === "scenes" || tab === "edit" || tab === "operations"
tab === "scenes" ||
tab === "images" ||
tab === "edit" ||
tab === "operations"
? tab
: "details";
const setActiveTabKey = (newTab: string | null) => {
@@ -152,6 +156,9 @@ export const Performer: React.FC = () => {
<Tab eventKey="scenes" title="Scenes">
<PerformerScenesPanel performer={performer} />
</Tab>
<Tab eventKey="images" title="Images">
<PerformerImagesPanel performer={performer} />
</Tab>
<Tab eventKey="edit" title="Edit">
<PerformerDetailsPanel
performer={performer}

View File

@@ -0,0 +1,14 @@
import React from "react";
import * as GQL from "src/core/generated-graphql";
import { ImageList } from "src/components/Images/ImageList";
import { performerFilterHook } from "src/core/performers";
interface IPerformerImagesPanel {
performer: Partial<GQL.PerformerDataFragment>;
}
export const PerformerImagesPanel: React.FC<IPerformerImagesPanel> = ({
performer,
}) => {
return <ImageList filterHook={performerFilterHook(performer)} />;
};

View File

@@ -1,8 +1,7 @@
import React from "react";
import * as GQL from "src/core/generated-graphql";
import { PerformersCriterion } from "src/models/list-filter/criteria/performers";
import { ListFilterModel } from "src/models/list-filter/filter";
import { SceneList } from "src/components/Scenes/SceneList";
import { performerFilterHook } from "src/core/performers";
interface IPerformerDetailsProps {
performer: Partial<GQL.PerformerDataFragment>;
@@ -11,37 +10,5 @@ interface IPerformerDetailsProps {
export const PerformerScenesPanel: React.FC<IPerformerDetailsProps> = ({
performer,
}) => {
function filterHook(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;
}
return <SceneList filterHook={filterHook} />;
return <SceneList filterHook={performerFilterHook(performer)} />;
};

View File

@@ -11,12 +11,14 @@ import {
useAllPerformersForFilter,
useMarkerStrings,
useScrapePerformerList,
useValidGalleriesForScene,
useTagCreate,
useStudioCreate,
usePerformerCreate,
useFindGalleries,
} from "src/core/StashService";
import { useToast } from "src/hooks";
import { ListFilterModel } from "src/models/list-filter/filter";
import { FilterMode } from "src/models/list-filter/types";
type ValidTypes =
| GQL.SlimPerformerDataFragment
@@ -90,12 +92,24 @@ const getSelectedValues = (selectedItems: ValueType<Option>) =>
: [];
export const SceneGallerySelect: React.FC<ISceneGallerySelect> = (props) => {
const { data, loading } = useValidGalleriesForScene(props.sceneId);
const galleries = data?.validGalleriesForScene ?? [];
const items = (galleries.length > 0
? [{ path: "None", id: "0" }, ...galleries]
: []
).map((g) => ({ label: g.title ?? "", value: g.id }));
const [query, setQuery] = React.useState<string>("");
const { data, loading } = useFindGalleries(getFilter());
const galleries = data?.findGalleries.galleries ?? [];
const items = galleries.map((g) => ({
label: g.title ?? g.path ?? "",
value: g.id,
}));
function getFilter() {
const ret = new ListFilterModel(FilterMode.Galleries);
ret.searchTerm = query;
return ret;
}
const onInputChange = debounce((input: string) => {
setQuery(input);
}, 500);
const onChange = (selectedItems: ValueType<Option>) => {
const selectedItem = getSelectedValues(selectedItems)[0];
@@ -110,8 +124,8 @@ export const SceneGallerySelect: React.FC<ISceneGallerySelect> = (props) => {
return (
<SelectComponent
className="input-control"
onChange={onChange}
onInputChange={onInputChange}
isLoading={loading}
items={items}
selectedOptions={selectedOptions}

View File

@@ -3,26 +3,27 @@ import { useStats } from "src/core/StashService";
import { FormattedMessage, FormattedNumber } from "react-intl";
import { LoadingIndicator } from "src/components/Shared";
import Changelog from "src/components/Changelog/Changelog";
import { TextUtils } from "src/utils";
export const Stats: React.FC = () => {
const { data, error, loading } = useStats();
if (error) return <span>{error.message}</span>;
if (loading || !data) return <LoadingIndicator />;
if (error) return <span>error.message</span>;
const size = data.stats.scene_size_count.split(" ");
const scenesSize = TextUtils.fileSize(data.stats.scenes_size);
const imagesSize = TextUtils.fileSize(data.stats.images_size);
return (
<div className="mt-5">
<div className="col col-sm-8 m-sm-auto row stats">
<div className="stats-element">
<p className="title">
<FormattedNumber value={parseFloat(size[0])} />
{` ${size[1]}`}
<FormattedNumber value={Math.floor(scenesSize.size)} />
{` ${TextUtils.formatFileSizeUnit(scenesSize.unit)}`}
</p>
<p className="heading">
<FormattedMessage id="library-size" defaultMessage="Library size" />
<FormattedMessage id="scenes-size" defaultMessage="Scenes size" />
</p>
</div>
<div className="stats-element">
@@ -33,6 +34,25 @@ export const Stats: React.FC = () => {
<FormattedMessage id="scenes" defaultMessage="Scenes" />
</p>
</div>
<div className="stats-element">
<p className="title">
<FormattedNumber value={Math.floor(imagesSize.size)} />
{` ${TextUtils.formatFileSizeUnit(imagesSize.unit)}`}
</p>
<p className="heading">
<FormattedMessage id="images-size" defaultMessage="Images size" />
</p>
</div>
<div className="stats-element">
<p className="title">
<FormattedNumber value={data.stats.image_count} />
</p>
<p className="heading">
<FormattedMessage id="images" defaultMessage="Images" />
</p>
</div>
</div>
<div className="col col-sm-8 m-sm-auto row stats">
<div className="stats-element">
<p className="title">
<FormattedNumber value={data.stats.movie_count} />

View File

@@ -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 = () => {
<Tab eventKey="scenes" title="Scenes">
<StudioScenesPanel studio={studio} />
</Tab>
<Tab eventKey="images" title="Images">
<StudioImagesPanel studio={studio} />
</Tab>
<Tab eventKey="childstudios" title="Child Studios">
<StudioChildrenPanel studio={studio} />
</Tab>

View File

@@ -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<GQL.StudioDataFragment>;
}
export const StudioImagesPanel: React.FC<IStudioImagesPanel> = ({ studio }) => {
return <ImageList filterHook={studioFilterHook(studio)} />;
};

View File

@@ -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<GQL.StudioDataFragment>;
}
export const StudioScenesPanel: React.FC<IStudioScenesPanel> = ({ 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 <SceneList filterHook={filterHook} />;
return <SceneList filterHook={studioFilterHook(studio)} />;
};

View File

@@ -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 = () => {
<Tab eventKey="scenes" title="Scenes">
<TagScenesPanel tag={tag} />
</Tab>
<Tab eventKey="images" title="Images">
<TagImagesPanel tag={tag} />
</Tab>
<Tab eventKey="markers" title="Markers">
<TagMarkersPanel tag={tag} />
</Tab>

View File

@@ -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<ITagImagesPanel> = ({ tag }) => {
return <ImageList filterHook={tagFilterHook(tag)} />;
};

View File

@@ -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<ITagScenesPanel> = ({ 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 <SceneList filterHook={filterHook} />;
return <SceneList filterHook={tagFilterHook(tag)} />;
};