Make a separate query for loading image query metadata

This commit is contained in:
WithoutPants 2025-12-03 17:56:56 +11:00
parent 39fd8a6550
commit 3494bfb197
6 changed files with 108 additions and 27 deletions

View file

@ -9,14 +9,27 @@ query FindImages(
image_ids: $image_ids image_ids: $image_ids
) { ) {
count count
megapixels
filesize
images { images {
...SlimImageData ...SlimImageData
} }
} }
} }
query FindImagesMetadata(
$filter: FindFilterType
$image_filter: ImageFilterType
$image_ids: [Int!]
) {
findImages(
filter: $filter
image_filter: $image_filter
image_ids: $image_ids
) {
megapixels
filesize
}
}
query FindImage($id: ID!, $checksum: String) { query FindImage($id: ID!, $checksum: String) {
findImage(id: $id, checksum: $checksum) { findImage(id: $id, checksum: $checksum) {
...ImageData ...ImageData

View file

@ -4,7 +4,11 @@ import cloneDeep from "lodash-es/cloneDeep";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import Mousetrap from "mousetrap"; import Mousetrap from "mousetrap";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { queryFindImages, useFindImages } from "src/core/StashService"; import {
queryFindImages,
useFindImages,
useFindImagesMetadata,
} from "src/core/StashService";
import { ItemList, ItemListContext, showWhenSelected } from "../List/ItemList"; import { ItemList, ItemListContext, showWhenSelected } from "../List/ItemList";
import { useLightbox } from "src/hooks/Lightbox/hooks"; import { useLightbox } from "src/hooks/Lightbox/hooks";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
@ -269,9 +273,17 @@ function getCount(result: GQL.FindImagesQueryResult) {
return result?.data?.findImages?.count ?? 0; return result?.data?.findImages?.count ?? 0;
} }
function renderMetadataByline(result: GQL.FindImagesQueryResult) { function renderMetadataByline(
const megapixels = result?.data?.findImages?.megapixels; result: GQL.FindImagesQueryResult,
const size = result?.data?.findImages?.filesize; metadataInfo?: GQL.FindImagesMetadataQueryResult
) {
const megapixels = metadataInfo?.data?.findImages?.megapixels;
const size = metadataInfo?.data?.findImages?.filesize;
if (metadataInfo?.loading) {
// return ellipsis
return <span className="images-stats">&nbsp;(...)</span>;
}
if (!megapixels && !size) { if (!megapixels && !size) {
return; return;
@ -450,6 +462,7 @@ export const ImageList: React.FC<IImageList> = ({
<ItemListContext <ItemListContext
filterMode={filterMode} filterMode={filterMode}
useResult={useFindImages} useResult={useFindImages}
useMetadataInfo={useFindImagesMetadata}
getItems={getItems} getItems={getItems}
getCount={getCount} getCount={getCount}
alterQuery={alterQuery} alterQuery={alterQuery}

View file

@ -112,7 +112,7 @@ export function useFilteredItemList<
}; };
} }
interface IItemListProps<T extends QueryResult, E extends IHasID> { interface IItemListProps<T extends QueryResult, E extends IHasID, M = unknown> {
view?: View; view?: View;
otherOperations?: IItemListOperation<T>[]; otherOperations?: IItemListOperation<T>[];
renderContent: ( renderContent: (
@ -123,7 +123,7 @@ interface IItemListProps<T extends QueryResult, E extends IHasID> {
onChangePage: (page: number) => void, onChangePage: (page: number) => void,
pageCount: number pageCount: number
) => React.ReactNode; ) => React.ReactNode;
renderMetadataByline?: (data: T) => React.ReactNode; renderMetadataByline?: (data: T, metadataInfo?: M) => React.ReactNode;
renderEditDialog?: ( renderEditDialog?: (
selected: E[], selected: E[],
onClose: (applied: boolean) => void onClose: (applied: boolean) => void
@ -140,8 +140,8 @@ interface IItemListProps<T extends QueryResult, E extends IHasID> {
renderToolbar?: (props: IFilteredListToolbar) => React.ReactNode; renderToolbar?: (props: IFilteredListToolbar) => React.ReactNode;
} }
export const ItemList = <T extends QueryResult, E extends IHasID>( export const ItemList = <T extends QueryResult, E extends IHasID, M = unknown>(
props: IItemListProps<T, E> props: IItemListProps<T, E, M>
) => { ) => {
const { const {
view, view,
@ -155,8 +155,8 @@ export const ItemList = <T extends QueryResult, E extends IHasID>(
} = props; } = props;
const { filter, setFilter: updateFilter } = useFilter(); const { filter, setFilter: updateFilter } = useFilter();
const { effectiveFilter, result, cachedResult, totalCount } = const { effectiveFilter, result, metadataInfo, cachedResult, totalCount } =
useQueryResultContext<T, E>(); useQueryResultContext<T, E, M>();
const listSelect = useListContext<E>(); const listSelect = useListContext<E>();
const { const {
selectedIds, selectedIds,
@ -174,8 +174,8 @@ export const ItemList = <T extends QueryResult, E extends IHasID>(
const metadataByline = useMemo(() => { const metadataByline = useMemo(() => {
if (cachedResult.loading) return ""; if (cachedResult.loading) return "";
return renderMetadataByline?.(cachedResult) ?? ""; return renderMetadataByline?.(cachedResult, metadataInfo) ?? "";
}, [renderMetadataByline, cachedResult]); }, [renderMetadataByline, cachedResult, metadataInfo]);
const pages = Math.ceil(totalCount / filter.itemsPerPage); const pages = Math.ceil(totalCount / filter.itemsPerPage);
@ -369,11 +369,16 @@ export const ItemList = <T extends QueryResult, E extends IHasID>(
); );
}; };
interface IItemListContextProps<T extends QueryResult, E extends IHasID> { interface IItemListContextProps<
T extends QueryResult,
E extends IHasID,
M = unknown
> {
filterMode: GQL.FilterMode; filterMode: GQL.FilterMode;
defaultSort?: string; defaultSort?: string;
defaultFilter?: ListFilterModel; defaultFilter?: ListFilterModel;
useResult: (filter: ListFilterModel) => T; useResult: (filter: ListFilterModel) => T;
useMetadataInfo?: (filter: ListFilterModel) => M;
getCount: (data: T) => number; getCount: (data: T) => number;
getItems: (data: T) => E[]; getItems: (data: T) => E[];
filterHook?: (filter: ListFilterModel) => ListFilterModel; filterHook?: (filter: ListFilterModel) => ListFilterModel;
@ -384,14 +389,19 @@ interface IItemListContextProps<T extends QueryResult, E extends IHasID> {
// Provides the contexts for the ItemList component. Includes functionality to scroll // Provides the contexts for the ItemList component. Includes functionality to scroll
// to top on page change. // to top on page change.
export const ItemListContext = <T extends QueryResult, E extends IHasID>( export const ItemListContext = <
props: PropsWithChildren<IItemListContextProps<T, E>> T extends QueryResult,
E extends IHasID,
M = unknown
>(
props: PropsWithChildren<IItemListContextProps<T, E, M>>
) => { ) => {
const { const {
filterMode, filterMode,
defaultSort, defaultSort,
defaultFilter: providedDefaultFilter, defaultFilter: providedDefaultFilter,
useResult, useResult,
useMetadataInfo,
getCount, getCount,
getItems, getItems,
view, view,
@ -425,6 +435,7 @@ export const ItemListContext = <T extends QueryResult, E extends IHasID>(
<QueryResultContext <QueryResultContext
filterHook={filterHook} filterHook={filterHook}
useResult={useResult} useResult={useResult}
useMetadataInfo={useMetadataInfo}
getCount={getCount} getCount={getCount}
getItems={getItems} getItems={getItems}
> >

View file

@ -80,21 +80,25 @@ export function useListContextOptional<T extends IHasID = IHasID>() {
interface IQueryResultContextOptions< interface IQueryResultContextOptions<
T extends QueryResult, T extends QueryResult,
E extends IHasID = IHasID E extends IHasID = IHasID,
M = unknown
> { > {
filterHook?: (filter: ListFilterModel) => ListFilterModel; filterHook?: (filter: ListFilterModel) => ListFilterModel;
useResult: (filter: ListFilterModel) => T; useResult: (filter: ListFilterModel) => T;
useMetadataInfo?: (filter: ListFilterModel) => M;
getCount: (data: T) => number; getCount: (data: T) => number;
getItems: (data: T) => E[]; getItems: (data: T) => E[];
} }
export interface IQueryResultContextState< export interface IQueryResultContextState<
T extends QueryResult = QueryResult, T extends QueryResult = QueryResult,
E extends IHasID = IHasID E extends IHasID = IHasID,
M = unknown
> { > {
effectiveFilter: ListFilterModel; effectiveFilter: ListFilterModel;
result: T; result: T;
cachedResult: T; cachedResult: T;
metadataInfo?: M;
items: E[]; items: E[];
totalCount: number; totalCount: number;
} }
@ -104,15 +108,23 @@ export const QueryResultStateContext =
export const QueryResultContext = < export const QueryResultContext = <
T extends QueryResult, T extends QueryResult,
E extends IHasID = IHasID E extends IHasID = IHasID,
M = unknown
>( >(
props: IQueryResultContextOptions<T, E> & { props: IQueryResultContextOptions<T, E, M> & {
children?: children?:
| ((props: IQueryResultContextState<T, E>) => React.ReactNode) | ((props: IQueryResultContextState<T, E, M>) => React.ReactNode)
| React.ReactNode; | React.ReactNode;
} }
) => { ) => {
const { filterHook, useResult, getItems, getCount, children } = props; const {
filterHook,
useResult,
useMetadataInfo,
getItems,
getCount,
children,
} = props;
const { filter } = useFilter(); const { filter } = useFilter();
const effectiveFilter = useMemo(() => { const effectiveFilter = useMemo(() => {
@ -122,9 +134,17 @@ export const QueryResultContext = <
return filter; return filter;
}, [filter, filterHook]); }, [filter, filterHook]);
// metadata filter is the effective filter with the sort, page size and page number removed
const metadataFilter = useMemo(
() => effectiveFilter.metadataInfo(),
[effectiveFilter]
);
const result = useResult(effectiveFilter); const result = useResult(effectiveFilter);
// use cached query result for pagination and metadata rendering const metadataInfo = useMetadataInfo?.(metadataFilter);
// use cached query result for pagination
const cachedResult = useCachedQueryResult(effectiveFilter, result); const cachedResult = useCachedQueryResult(effectiveFilter, result);
const items = useMemo(() => getItems(result), [getItems, result]); const items = useMemo(() => getItems(result), [getItems, result]);
@ -133,12 +153,13 @@ export const QueryResultContext = <
[getCount, cachedResult] [getCount, cachedResult]
); );
const state: IQueryResultContextState<T, E> = { const state: IQueryResultContextState<T, E, M> = {
effectiveFilter, effectiveFilter,
result, result,
cachedResult, cachedResult,
items, items,
totalCount, totalCount,
metadataInfo,
}; };
return ( return (
@ -154,7 +175,8 @@ export const QueryResultContext = <
export function useQueryResultContext< export function useQueryResultContext<
T extends QueryResult, T extends QueryResult,
E extends IHasID = IHasID E extends IHasID = IHasID,
M = unknown
>() { >() {
const context = React.useContext(QueryResultStateContext); const context = React.useContext(QueryResultStateContext);
@ -164,5 +186,5 @@ export function useQueryResultContext<
); );
} }
return context as IQueryResultContextState<T, E>; return context as IQueryResultContextState<T, E, M>;
} }

View file

@ -201,6 +201,15 @@ export const useFindImages = (filter?: ListFilterModel) =>
}, },
}); });
export const useFindImagesMetadata = (filter?: ListFilterModel) =>
GQL.useFindImagesMetadataQuery({
skip: filter === undefined,
variables: {
filter: filter?.makeFindFilter(),
image_filter: filter?.makeFilter(),
},
});
export const queryFindImages = (filter: ListFilterModel) => export const queryFindImages = (filter: ListFilterModel) =>
client.query<GQL.FindImagesQuery>({ client.query<GQL.FindImagesQuery>({
query: GQL.FindImagesDocument, query: GQL.FindImagesDocument,

View file

@ -103,6 +103,19 @@ export class ListFilterModel {
}); });
} }
// returns a clone of the filter for metadata fetching
// this removes the sort, page size and page number and zoom index
public metadataInfo() {
const clone = this.clone();
clone.sortBy = undefined;
clone.randomSeed = -1;
clone.currentPage = 1;
clone.itemsPerPage = 0;
clone.zoomIndex = 1;
clone.displayMode = DEFAULT_PARAMS.displayMode;
return clone;
}
// returns the number of filters applied // returns the number of filters applied
public count() { public count() {
// don't include search term // don't include search term