From 3494bfb1976d2d4d7e8eb6ef507518a5a6fbca36 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Wed, 3 Dec 2025 17:56:56 +1100 Subject: [PATCH] Make a separate query for loading image query metadata --- ui/v2.5/graphql/queries/image.graphql | 17 +++++++- ui/v2.5/src/components/Images/ImageList.tsx | 21 ++++++++-- ui/v2.5/src/components/List/ItemList.tsx | 33 ++++++++++----- ui/v2.5/src/components/List/ListProvider.tsx | 42 +++++++++++++++----- ui/v2.5/src/core/StashService.ts | 9 +++++ ui/v2.5/src/models/list-filter/filter.ts | 13 ++++++ 6 files changed, 108 insertions(+), 27 deletions(-) diff --git a/ui/v2.5/graphql/queries/image.graphql b/ui/v2.5/graphql/queries/image.graphql index ee96d00d2..d2c6cdac8 100644 --- a/ui/v2.5/graphql/queries/image.graphql +++ b/ui/v2.5/graphql/queries/image.graphql @@ -9,14 +9,27 @@ query FindImages( image_ids: $image_ids ) { count - megapixels - filesize images { ...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) { findImage(id: $id, checksum: $checksum) { ...ImageData diff --git a/ui/v2.5/src/components/Images/ImageList.tsx b/ui/v2.5/src/components/Images/ImageList.tsx index 0e3753480..23e93a94c 100644 --- a/ui/v2.5/src/components/Images/ImageList.tsx +++ b/ui/v2.5/src/components/Images/ImageList.tsx @@ -4,7 +4,11 @@ import cloneDeep from "lodash-es/cloneDeep"; import { useHistory } from "react-router-dom"; import Mousetrap from "mousetrap"; 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 { useLightbox } from "src/hooks/Lightbox/hooks"; import { ListFilterModel } from "src/models/list-filter/filter"; @@ -269,9 +273,17 @@ function getCount(result: GQL.FindImagesQueryResult) { return result?.data?.findImages?.count ?? 0; } -function renderMetadataByline(result: GQL.FindImagesQueryResult) { - const megapixels = result?.data?.findImages?.megapixels; - const size = result?.data?.findImages?.filesize; +function renderMetadataByline( + result: GQL.FindImagesQueryResult, + metadataInfo?: GQL.FindImagesMetadataQueryResult +) { + const megapixels = metadataInfo?.data?.findImages?.megapixels; + const size = metadataInfo?.data?.findImages?.filesize; + + if (metadataInfo?.loading) { + // return ellipsis + return  (...); + } if (!megapixels && !size) { return; @@ -450,6 +462,7 @@ export const ImageList: React.FC = ({ { +interface IItemListProps { view?: View; otherOperations?: IItemListOperation[]; renderContent: ( @@ -123,7 +123,7 @@ interface IItemListProps { onChangePage: (page: number) => void, pageCount: number ) => React.ReactNode; - renderMetadataByline?: (data: T) => React.ReactNode; + renderMetadataByline?: (data: T, metadataInfo?: M) => React.ReactNode; renderEditDialog?: ( selected: E[], onClose: (applied: boolean) => void @@ -140,8 +140,8 @@ interface IItemListProps { renderToolbar?: (props: IFilteredListToolbar) => React.ReactNode; } -export const ItemList = ( - props: IItemListProps +export const ItemList = ( + props: IItemListProps ) => { const { view, @@ -155,8 +155,8 @@ export const ItemList = ( } = props; const { filter, setFilter: updateFilter } = useFilter(); - const { effectiveFilter, result, cachedResult, totalCount } = - useQueryResultContext(); + const { effectiveFilter, result, metadataInfo, cachedResult, totalCount } = + useQueryResultContext(); const listSelect = useListContext(); const { selectedIds, @@ -174,8 +174,8 @@ export const ItemList = ( const metadataByline = useMemo(() => { if (cachedResult.loading) return ""; - return renderMetadataByline?.(cachedResult) ?? ""; - }, [renderMetadataByline, cachedResult]); + return renderMetadataByline?.(cachedResult, metadataInfo) ?? ""; + }, [renderMetadataByline, cachedResult, metadataInfo]); const pages = Math.ceil(totalCount / filter.itemsPerPage); @@ -369,11 +369,16 @@ export const ItemList = ( ); }; -interface IItemListContextProps { +interface IItemListContextProps< + T extends QueryResult, + E extends IHasID, + M = unknown +> { filterMode: GQL.FilterMode; defaultSort?: string; defaultFilter?: ListFilterModel; useResult: (filter: ListFilterModel) => T; + useMetadataInfo?: (filter: ListFilterModel) => M; getCount: (data: T) => number; getItems: (data: T) => E[]; filterHook?: (filter: ListFilterModel) => ListFilterModel; @@ -384,14 +389,19 @@ interface IItemListContextProps { // Provides the contexts for the ItemList component. Includes functionality to scroll // to top on page change. -export const ItemListContext = ( - props: PropsWithChildren> +export const ItemListContext = < + T extends QueryResult, + E extends IHasID, + M = unknown +>( + props: PropsWithChildren> ) => { const { filterMode, defaultSort, defaultFilter: providedDefaultFilter, useResult, + useMetadataInfo, getCount, getItems, view, @@ -425,6 +435,7 @@ export const ItemListContext = ( diff --git a/ui/v2.5/src/components/List/ListProvider.tsx b/ui/v2.5/src/components/List/ListProvider.tsx index 2e8854586..8b4ba6c70 100644 --- a/ui/v2.5/src/components/List/ListProvider.tsx +++ b/ui/v2.5/src/components/List/ListProvider.tsx @@ -80,21 +80,25 @@ export function useListContextOptional() { interface IQueryResultContextOptions< T extends QueryResult, - E extends IHasID = IHasID + E extends IHasID = IHasID, + M = unknown > { filterHook?: (filter: ListFilterModel) => ListFilterModel; useResult: (filter: ListFilterModel) => T; + useMetadataInfo?: (filter: ListFilterModel) => M; getCount: (data: T) => number; getItems: (data: T) => E[]; } export interface IQueryResultContextState< T extends QueryResult = QueryResult, - E extends IHasID = IHasID + E extends IHasID = IHasID, + M = unknown > { effectiveFilter: ListFilterModel; result: T; cachedResult: T; + metadataInfo?: M; items: E[]; totalCount: number; } @@ -104,15 +108,23 @@ export const QueryResultStateContext = export const QueryResultContext = < T extends QueryResult, - E extends IHasID = IHasID + E extends IHasID = IHasID, + M = unknown >( - props: IQueryResultContextOptions & { + props: IQueryResultContextOptions & { children?: - | ((props: IQueryResultContextState) => React.ReactNode) + | ((props: IQueryResultContextState) => React.ReactNode) | React.ReactNode; } ) => { - const { filterHook, useResult, getItems, getCount, children } = props; + const { + filterHook, + useResult, + useMetadataInfo, + getItems, + getCount, + children, + } = props; const { filter } = useFilter(); const effectiveFilter = useMemo(() => { @@ -122,9 +134,17 @@ export const QueryResultContext = < return filter; }, [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); - // 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 items = useMemo(() => getItems(result), [getItems, result]); @@ -133,12 +153,13 @@ export const QueryResultContext = < [getCount, cachedResult] ); - const state: IQueryResultContextState = { + const state: IQueryResultContextState = { effectiveFilter, result, cachedResult, items, totalCount, + metadataInfo, }; return ( @@ -154,7 +175,8 @@ export const QueryResultContext = < export function useQueryResultContext< T extends QueryResult, - E extends IHasID = IHasID + E extends IHasID = IHasID, + M = unknown >() { const context = React.useContext(QueryResultStateContext); @@ -164,5 +186,5 @@ export function useQueryResultContext< ); } - return context as IQueryResultContextState; + return context as IQueryResultContextState; } diff --git a/ui/v2.5/src/core/StashService.ts b/ui/v2.5/src/core/StashService.ts index d43d87097..424301fe1 100644 --- a/ui/v2.5/src/core/StashService.ts +++ b/ui/v2.5/src/core/StashService.ts @@ -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) => client.query({ query: GQL.FindImagesDocument, diff --git a/ui/v2.5/src/models/list-filter/filter.ts b/ui/v2.5/src/models/list-filter/filter.ts index 4780f1ab6..cc2cc7a77 100644 --- a/ui/v2.5/src/models/list-filter/filter.ts +++ b/ui/v2.5/src/models/list-filter/filter.ts @@ -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 public count() { // don't include search term