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