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 1/4] 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 From 8ad889d68c3f47ad830895a146df5089b9b509df Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Wed, 3 Dec 2025 18:20:39 +1100 Subject: [PATCH 2/4] Prevent unnecessary re-queries --- ui/v2.5/src/components/List/ListProvider.tsx | 12 +++++++----- ui/v2.5/src/components/List/util.ts | 13 +++++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/ui/v2.5/src/components/List/ListProvider.tsx b/ui/v2.5/src/components/List/ListProvider.tsx index 8b4ba6c70..4e824085d 100644 --- a/ui/v2.5/src/components/List/ListProvider.tsx +++ b/ui/v2.5/src/components/List/ListProvider.tsx @@ -1,5 +1,10 @@ import React, { useMemo } from "react"; -import { IListSelect, useCachedQueryResult, useListSelect } from "./util"; +import { + IListSelect, + useCachedQueryResult, + useListSelect, + useMetadataFilter, +} from "./util"; import { isFunction } from "lodash-es"; import { IHasID } from "src/utils/data"; import { useFilter } from "./FilterProvider"; @@ -135,10 +140,7 @@ export const QueryResultContext = < }, [filter, filterHook]); // metadata filter is the effective filter with the sort, page size and page number removed - const metadataFilter = useMemo( - () => effectiveFilter.metadataInfo(), - [effectiveFilter] - ); + const metadataFilter = useMetadataFilter(effectiveFilter); const result = useResult(effectiveFilter); diff --git a/ui/v2.5/src/components/List/util.ts b/ui/v2.5/src/components/List/util.ts index c15c3335a..e2bd561ca 100644 --- a/ui/v2.5/src/components/List/util.ts +++ b/ui/v2.5/src/components/List/util.ts @@ -483,6 +483,19 @@ export function useCachedQueryResult( return cachedResult; } +// used to generate a metadata info filter that only updates when necessary +export function useMetadataFilter(filter: ListFilterModel) { + const lastValue = usePrevious(filter); + + return useMemo(() => { + if (!lastValue || !totalCountImpacted(lastValue!, filter)) { + return filter.metadataInfo(); + } + + return lastValue; + }, [filter, lastValue]); +} + export interface IQueryResultHook< T extends QueryResult, E extends IHasID = IHasID From fbf13f7a8bf4c8247134618ba93333b635b24dcc Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Fri, 5 Dec 2025 19:10:56 +1100 Subject: [PATCH 3/4] Add missing field to metadataInfo --- ui/v2.5/src/models/list-filter/filter.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/v2.5/src/models/list-filter/filter.ts b/ui/v2.5/src/models/list-filter/filter.ts index cc2cc7a77..d28107a12 100644 --- a/ui/v2.5/src/models/list-filter/filter.ts +++ b/ui/v2.5/src/models/list-filter/filter.ts @@ -110,6 +110,7 @@ export class ListFilterModel { clone.sortBy = undefined; clone.randomSeed = -1; clone.currentPage = 1; + clone.sortDirection = DEFAULT_PARAMS.sortDirection; clone.itemsPerPage = 0; clone.zoomIndex = 1; clone.displayMode = DEFAULT_PARAMS.displayMode; From 5eae9c223e308e34a174e6b304d236070428fe18 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Fri, 5 Dec 2025 19:15:26 +1100 Subject: [PATCH 4/4] Simplify metadataFilter calculation --- ui/v2.5/src/components/List/ListProvider.tsx | 13 +++++-------- ui/v2.5/src/components/List/util.ts | 13 ------------- 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/ui/v2.5/src/components/List/ListProvider.tsx b/ui/v2.5/src/components/List/ListProvider.tsx index 4e824085d..0584a61c6 100644 --- a/ui/v2.5/src/components/List/ListProvider.tsx +++ b/ui/v2.5/src/components/List/ListProvider.tsx @@ -1,10 +1,5 @@ import React, { useMemo } from "react"; -import { - IListSelect, - useCachedQueryResult, - useListSelect, - useMetadataFilter, -} from "./util"; +import { IListSelect, useCachedQueryResult, useListSelect } from "./util"; import { isFunction } from "lodash-es"; import { IHasID } from "src/utils/data"; import { useFilter } from "./FilterProvider"; @@ -140,10 +135,12 @@ export const QueryResultContext = < }, [filter, filterHook]); // metadata filter is the effective filter with the sort, page size and page number removed - const metadataFilter = useMetadataFilter(effectiveFilter); + const metadataFilter = useMemo( + () => effectiveFilter.metadataInfo(), + [effectiveFilter] + ); const result = useResult(effectiveFilter); - const metadataInfo = useMetadataInfo?.(metadataFilter); // use cached query result for pagination diff --git a/ui/v2.5/src/components/List/util.ts b/ui/v2.5/src/components/List/util.ts index e2bd561ca..c15c3335a 100644 --- a/ui/v2.5/src/components/List/util.ts +++ b/ui/v2.5/src/components/List/util.ts @@ -483,19 +483,6 @@ export function useCachedQueryResult( return cachedResult; } -// used to generate a metadata info filter that only updates when necessary -export function useMetadataFilter(filter: ListFilterModel) { - const lastValue = usePrevious(filter); - - return useMemo(() => { - if (!lastValue || !totalCountImpacted(lastValue!, filter)) { - return filter.metadataInfo(); - } - - return lastValue; - }, [filter, lastValue]); -} - export interface IQueryResultHook< T extends QueryResult, E extends IHasID = IHasID