mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 04:44:37 +03:00
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:
@@ -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}
|
||||
|
||||
@@ -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)} />;
|
||||
};
|
||||
@@ -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)} />;
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)} />;
|
||||
};
|
||||
@@ -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)} />;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
12
ui/v2.5/src/components/Tags/TagDetails/TagImagesPanel.tsx
Normal file
12
ui/v2.5/src/components/Tags/TagDetails/TagImagesPanel.tsx
Normal 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)} />;
|
||||
};
|
||||
@@ -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)} />;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user