refactor: remove unused methods/routes, fix some gallery invalidation issues

This commit is contained in:
psychedelicious
2025-06-25 13:44:57 +10:00
parent 98368b0665
commit b2b42be51c
22 changed files with 139 additions and 657 deletions

View File

@@ -1,6 +1,6 @@
import { isAnyOf } from '@reduxjs/toolkit';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
import { selectListImagesBaseQueryArgs } from 'features/gallery/store/gallerySelectors';
import { boardIdSelected, galleryViewChanged, imageSelected } from 'features/gallery/store/gallerySlice';
import { imagesApi } from 'services/api/endpoints/images';
@@ -13,7 +13,7 @@ export const addBoardIdSelectedListener = (startAppListening: AppStartListening)
const state = getState();
const queryArgs = selectListImagesQueryArgs(state);
const queryArgs = { ...selectListImagesBaseQueryArgs(state), offset: 0 };
// wait until the board has some images - maybe it already has some from a previous fetch
// must use getState() to ensure we do not have stale state

View File

@@ -1,29 +1,9 @@
import { createAction } from '@reduxjs/toolkit';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import type { RootState } from 'app/store/store';
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
import { selectListImageNamesQueryArgs } from 'features/gallery/store/gallerySelectors';
import { imageToCompareChanged, selectionChanged } from 'features/gallery/store/gallerySlice';
import { uniq } from 'lodash-es';
import { imagesApi } from 'services/api/endpoints/images';
import type { ImageCategory, SQLiteDirection } from 'services/api/types';
// Type for image collection query arguments
type ImageCollectionQueryArgs = {
board_id?: string;
categories?: ImageCategory[];
search_term?: string;
order_dir?: SQLiteDirection;
is_intermediate: boolean;
};
/**
* Helper function to get cached image names list for selection operations
* Returns an ordered array of image names (starred first, then unstarred)
*/
const getCachedImageNames = (state: RootState, queryArgs: ImageCollectionQueryArgs): string[] => {
const queryResult = imagesApi.endpoints.getImageNames.select(queryArgs)(state);
return queryResult.data || [];
};
export const galleryImageClicked = createAction<{
imageName: string;
@@ -50,10 +30,8 @@ export const addGalleryImageClickedListener = (startAppListening: AppStartListen
effect: (action, { dispatch, getState }) => {
const { imageName, shiftKey, ctrlKey, metaKey, altKey } = action.payload;
const state = getState();
const queryArgs = selectListImagesQueryArgs(state);
// Get cached image names for selection operations
const imageNames = getCachedImageNames(state, queryArgs);
const queryArgs = selectListImageNamesQueryArgs(state);
const imageNames = imagesApi.endpoints.getImageNames.select(queryArgs)(state).data ?? [];
// If we don't have the image names cached, we can't perform selection operations
// This can happen if the user clicks on an image before the names are loaded

View File

@@ -10,7 +10,6 @@ import {
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import type { CanvasState, RefImagesState } from 'features/controlLayers/store/types';
import type { ImageUsage } from 'features/deleteImageModal/store/types';
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
import { imageSelected } from 'features/gallery/store/gallerySlice';
import { fieldImageCollectionValueChanged, fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import { selectNodesSlice } from 'features/nodes/store/selectors';
@@ -81,14 +80,8 @@ const handleDeletions = async (image_names: string[], dispatch: AppDispatch, get
await dispatch(imagesApi.endpoints.deleteImages.initiate({ image_names }, { track: false })).unwrap();
if (intersection(state.gallery.selection, image_names).length > 0) {
// Some selected images were deleted, need to select the next image
const queryArgs = selectListImagesQueryArgs(state);
const { data } = imagesApi.endpoints.listImages.select(queryArgs)(state);
if (data) {
// When we delete multiple images, we clear the selection. Then, the the next time we load images, we will
// select the first one. This is handled below in the listener for `imagesApi.endpoints.listImages.matchFulfilled`.
dispatch(imageSelected(null));
}
// Some selected images were deleted, clear selection
dispatch(imageSelected(null));
}
// We need to reset the features where the image is in use - none of these work if their image(s) don't exist

View File

@@ -5,6 +5,7 @@ import { CanvasAlertsInvocationProgress } from 'features/controlLayers/component
import { DndImage } from 'features/dnd/DndImage';
import ImageMetadataViewer from 'features/gallery/components/ImageMetadataViewer/ImageMetadataViewer';
import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
import { selectAutoSwitch } from 'features/gallery/store/gallerySelectors';
import type { ProgressImage as ProgressImageType } from 'features/nodes/types/common';
import { selectShouldShowImageDetails, selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors';
import type { AnimationProps } from 'framer-motion';
@@ -21,6 +22,7 @@ import { ProgressIndicator } from './ProgressIndicator2';
export const CurrentImagePreview = memo(({ imageDTO }: { imageDTO: ImageDTO | null }) => {
const shouldShowImageDetails = useAppSelector(selectShouldShowImageDetails);
const shouldShowProgressInViewer = useAppSelector(selectShouldShowProgressInViewer);
const autoSwitch = useAppSelector(selectAutoSwitch);
const socket = useStore($socket);
const [progressEvent, setProgressEvent] = useState<S['InvocationProgressEvent'] | null>(null);
@@ -58,6 +60,29 @@ export const CurrentImagePreview = memo(({ imageDTO }: { imageDTO: ImageDTO | nu
};
}, [socket]);
useEffect(() => {
if (!socket) {
return;
}
if (autoSwitch) {
return;
}
// When auto-switch is enabled, we will get a load event as we switch to the new image. This in turn clears the progress image,
// creating the illusion of the progress image turning into the new image.
// But when auto-switch is disabled, we won't get that load event, so we need to clear the progress image manually.
const onQueueItemStatusChanged = () => {
setProgressEvent(null);
setProgressImage(null);
};
socket.on('queue_item_status_changed', onQueueItemStatusChanged);
return () => {
socket.off('queue_item_status_changed', onQueueItemStatusChanged);
};
}, [autoSwitch, socket]);
const onLoadImage = useCallback(() => {
if (!progressEvent || !imageDTO) {
return;

View File

@@ -4,10 +4,11 @@ import { logger } from 'app/logging/logger';
import { EMPTY_ARRAY } from 'app/store/constants';
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
import {
LIMIT,
selectGalleryImageMinimumWidth,
selectImageToCompare,
selectLastSelectedImage,
selectListImagesQueryArgs,
selectListImageNamesQueryArgs,
} from 'features/gallery/store/gallerySelectors';
import { imageToCompareChanged, selectionChanged } from 'features/gallery/store/gallerySlice';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
@@ -37,31 +38,26 @@ const SCROLL_SEEK_VELOCITY_THRESHOLD = 4096;
const DEBOUNCE_DELAY = 500;
const SPINNER_OPACITY = 0.3;
type ListImagesQueryArgs = ReturnType<typeof selectListImagesQueryArgs>;
type ListImageNamesQueryArgs = ReturnType<typeof selectListImageNamesQueryArgs>;
type GridContext = {
queryArgs: ListImagesQueryArgs;
queryArgs: ListImageNamesQueryArgs;
imageNames: string[];
};
export const useDebouncedListImagesQueryArgs = () => {
const _galleryQueryArgs = useAppSelector(selectListImagesQueryArgs);
const [queryArgs] = useDebounce(_galleryQueryArgs, DEBOUNCE_DELAY);
return queryArgs;
};
// Hook to get an image DTO from cache or trigger loading
const useImageDTOFromListQuery = (
index: number,
imageName: string,
queryArgs: ListImagesQueryArgs
queryArgs: ListImageNamesQueryArgs
): ImageDTO | null => {
const { arg, options } = useMemo(() => {
const pageOffset = Math.floor(index / queryArgs.limit) * queryArgs.limit;
const pageOffset = Math.floor(index / LIMIT) * LIMIT;
return {
arg: {
...queryArgs,
offset: pageOffset,
limit: LIMIT,
} satisfies Parameters<typeof useListImagesQuery>[0],
options: {
selectFromResult: ({ data }) => {
@@ -82,7 +78,7 @@ const useImageDTOFromListQuery = (
// Individual image component that gets its data from RTK Query cache
const ImageAtPosition = memo(
({ index, queryArgs, imageName }: { index: number; imageName: string; queryArgs: ListImagesQueryArgs }) => {
({ index, queryArgs, imageName }: { index: number; imageName: string; queryArgs: ListImageNamesQueryArgs }) => {
const imageDTO = useImageDTOFromListQuery(index, imageName, queryArgs);
if (!imageDTO) {
@@ -408,7 +404,8 @@ const getImageNamesQueryOptions = {
} satisfies Parameters<typeof useGetImageNamesQuery>[1];
export const useGalleryImageNames = () => {
const queryArgs = useDebouncedListImagesQueryArgs();
const _queryArgs = useAppSelector(selectListImageNamesQueryArgs);
const [queryArgs] = useDebounce(_queryArgs, DEBOUNCE_DELAY);
const { imageNames, isLoading, isFetching } = useGetImageNamesQuery(queryArgs, getImageNamesQueryOptions);
return { imageNames, isLoading, isFetching, queryArgs };
};

View File

@@ -2,8 +2,7 @@ import { createSelector } from '@reduxjs/toolkit';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { selectGallerySlice } from 'features/gallery/store/gallerySlice';
import { ASSETS_CATEGORIES, IMAGE_CATEGORIES } from 'features/gallery/store/types';
import type { ListBoardsArgs, ListImagesArgs } from 'services/api/types';
import type { SetNonNullable } from 'type-fest';
import type { ListBoardsArgs } from 'services/api/types';
export const selectFirstSelectedImage = createSelector(selectGallerySlice, (gallery) => gallery.selection.at(0));
export const selectLastSelectedImage = createSelector(selectGallerySlice, (gallery) => gallery.selection.at(-1));
@@ -28,7 +27,7 @@ export const selectGallerySearchTerm = createSelector(selectGallerySlice, (galle
export const selectGalleryOrderDir = createSelector(selectGallerySlice, (gallery) => gallery.orderDir);
export const selectGalleryStarredFirst = createSelector(selectGallerySlice, (gallery) => gallery.starredFirst);
export const selectListImagesQueryArgs = createMemoizedSelector(
export const selectListImageNamesQueryArgs = createMemoizedSelector(
[
selectSelectedBoardId,
selectGalleryQueryCategories,
@@ -36,17 +35,20 @@ export const selectListImagesQueryArgs = createMemoizedSelector(
selectGalleryOrderDir,
selectGalleryStarredFirst,
],
(board_id, categories, search_term, order_dir, starred_first) =>
({
board_id,
categories,
search_term,
order_dir,
starred_first,
is_intermediate: false, // We don't show intermediate images in the gallery
limit: 100, // Page size is _always_ 100
}) satisfies SetNonNullable<ListImagesArgs, 'limit'>
(board_id, categories, search_term, order_dir, starred_first) => ({
board_id,
categories,
search_term,
order_dir,
starred_first,
is_intermediate: false,
})
);
export const LIMIT = 100;
export const selectListImagesBaseQueryArgs = createMemoizedSelector(selectListImageNamesQueryArgs, (baseQueryArgs) => ({
...baseQueryArgs,
limit: LIMIT,
}));
export const selectAutoAssignBoardOnClick = createSelector(
selectGallerySlice,
(gallery) => gallery.autoAssignBoardOnClick

View File

@@ -427,61 +427,12 @@ export const imagesApi = api.injectEndpoints({
},
}),
}),
/**
* Get counts for starred and unstarred image collections
*/
getImageCollectionCounts: build.query<
paths['/api/v1/images/collections/counts']['get']['responses']['200']['content']['application/json'],
paths['/api/v1/images/collections/counts']['get']['parameters']['query']
>({
query: (queryArgs) => ({
url: buildImagesUrl('collections/counts', queryArgs),
method: 'GET',
}),
providesTags: ['ImageCollectionCounts', 'FetchOnReconnect'],
}),
/**
* Get images from a specific collection (starred or unstarred)
*/
getImageCollection: build.query<
paths['/api/v1/images/collections/{collection}']['get']['responses']['200']['content']['application/json'],
paths['/api/v1/images/collections/{collection}']['get']['parameters']['path'] &
paths['/api/v1/images/collections/{collection}']['get']['parameters']['query']
>({
query: ({ collection, ...queryArgs }) => ({
url: buildImagesUrl(`collections/${collection}`, queryArgs),
method: 'GET',
}),
providesTags: (result, error, { collection, board_id, categories }) => {
const cacheKey = `${collection}-${board_id || 'all'}-${categories?.join(',') || 'all'}`;
return [
{ type: 'ImageCollection', id: collection },
{ type: 'ImageCollection', id: cacheKey },
'FetchOnReconnect',
];
},
async onQueryStarted(_, { dispatch, queryFulfilled }) {
// Populate the getImageDTO cache with these images, similar to listImages
const res = await queryFulfilled;
const imageDTOs = res.data.items;
const updates: Param0<typeof imagesApi.util.upsertQueryEntries> = [];
for (const imageDTO of imageDTOs) {
updates.push({
endpointName: 'getImageDTO',
arg: imageDTO.image_name,
value: imageDTO,
});
}
dispatch(imagesApi.util.upsertQueryEntries(updates));
},
}),
/**
* Get ordered list of image names for selection operations
*/
getImageNames: build.query<
string[],
{
image_origin?: 'internal' | 'external' | null;
categories?: ImageCategory[] | null;
is_intermediate?: boolean | null;
board_id?: string | null;
@@ -493,46 +444,11 @@ export const imagesApi = api.injectEndpoints({
url: buildImagesUrl('names', queryArgs),
method: 'GET',
}),
providesTags: ['ImageNameList', 'FetchOnReconnect'],
}),
/**
* Get paginated images with starred first (unified list)
*/
getUnifiedImageList: build.query<
ListImagesResponse,
{
offset?: number;
limit?: number;
image_origin?: 'internal' | 'external' | null;
categories?: ImageCategory[] | null;
is_intermediate?: boolean | null;
board_id?: string | null;
search_term?: string | null;
order_dir?: SQLiteDirection;
}
>({
query: (queryArgs) => ({
url: getListImagesUrl({ ...queryArgs, starred_first: true }),
method: 'GET',
}),
providesTags: (result, error, { board_id, categories }) => [
{ type: 'ImageList', id: getListImagesUrl({ board_id, categories }) },
providesTags: (result, error, queryArgs) => [
'ImageNameList',
'FetchOnReconnect',
{ type: 'ImageNameList', id: stableHash(queryArgs) },
],
async onQueryStarted(_, { dispatch, queryFulfilled }) {
// Populate the getImageDTO cache with these images
const res = await queryFulfilled;
const imageDTOs = res.data.items;
const updates: Param0<typeof imagesApi.util.upsertQueryEntries> = [];
for (const imageDTO of imageDTOs) {
updates.push({
endpointName: 'getImageDTO',
arg: imageDTO.image_name,
value: imageDTO,
});
}
dispatch(imagesApi.util.upsertQueryEntries(updates));
},
}),
}),
});
@@ -555,11 +471,7 @@ export const {
useStarImagesMutation,
useUnstarImagesMutation,
useBulkDownloadImagesMutation,
useGetImageCollectionCountsQuery,
useGetImageCollectionQuery,
useLazyGetImageCollectionQuery,
useGetImageNamesQuery,
useGetUnifiedImageListQuery,
} = imagesApi;
/**

View File

@@ -752,7 +752,7 @@ export type paths = {
patch?: never;
trace?: never;
};
"/api/v1/images/collections/counts": {
"/api/v1/images/names": {
parameters: {
query?: never;
header?: never;
@@ -760,30 +760,10 @@ export type paths = {
cookie?: never;
};
/**
* Get Image Collection Counts
* @description Gets counts for starred and unstarred image collections
* Get Image Names
* @description Gets ordered list of all image names (starred first, then unstarred)
*/
get: operations["get_image_collection_counts"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/images/collections/{collection}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Get Image Collection
* @description Gets images from a specific collection (starred or unstarred)
*/
get: operations["get_image_collection"];
get: operations["get_image_names"];
put?: never;
post?: never;
delete?: never;
@@ -9844,19 +9824,6 @@ export type components = {
*/
type: "img_channel_offset";
};
/** ImageCollectionCounts */
ImageCollectionCounts: {
/**
* Starred Count
* @description The number of starred images in the collection.
*/
starred_count: number;
/**
* Unstarred Count
* @description The number of unstarred images in the collection.
*/
unstarred_count: number;
};
/**
* Image Collection Primitive
* @description A collection of image primitive values
@@ -23728,17 +23695,21 @@ export interface operations {
};
};
};
get_image_collection_counts: {
get_image_names: {
parameters: {
query?: {
/** @description The origin of images to count. */
/** @description The origin of images to list. */
image_origin?: components["schemas"]["ResourceOrigin"] | null;
/** @description The categories of image to include. */
categories?: components["schemas"]["ImageCategory"][] | null;
/** @description Whether to include intermediate images. */
/** @description Whether to list intermediate images. */
is_intermediate?: boolean | null;
/** @description The board id to filter by. Use 'none' to find images without a board. */
board_id?: string | null;
/** @description The order of sort */
order_dir?: components["schemas"]["SQLiteDirection"];
/** @description Whether to sort by starred images first */
starred_first?: boolean;
/** @description The term to search for */
search_term?: string | null;
};
@@ -23754,56 +23725,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ImageCollectionCounts"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
get_image_collection: {
parameters: {
query?: {
/** @description The origin of images to list. */
image_origin?: components["schemas"]["ResourceOrigin"] | null;
/** @description The categories of image to include. */
categories?: components["schemas"]["ImageCategory"][] | null;
/** @description Whether to list intermediate images. */
is_intermediate?: boolean | null;
/** @description The board id to filter by. Use 'none' to find images without a board. */
board_id?: string | null;
/** @description The offset within the collection */
offset?: number;
/** @description The number of images to return */
limit?: number;
/** @description The order of sort */
order_dir?: components["schemas"]["SQLiteDirection"];
/** @description The term to search for */
search_term?: string | null;
};
header?: never;
path: {
/** @description The collection to retrieve from */
collection: "starred" | "unstarred";
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["OffsetPaginatedResults_ImageDTO_"];
"application/json": string[];
};
};
/** @description Validation Error */

View File

@@ -5,7 +5,7 @@ import { deepClone } from 'common/util/deepClone';
import {
selectAutoSwitch,
selectGalleryView,
selectListImagesQueryArgs,
selectListImagesBaseQueryArgs,
selectSelectedBoardId,
} from 'features/gallery/store/gallerySelectors';
import { boardIdSelected, galleryViewChanged, imageSelected } from 'features/gallery/store/gallerySlice';
@@ -44,7 +44,7 @@ export const buildOnInvocationComplete = (getState: AppGetState, dispatch: AppDi
const boardTotalAdditions: Record<string, number> = {};
const boardTagIdsToInvalidate: Set<string> = new Set();
const imageListTagIdsToInvalidate: Set<string> = new Set();
const listImagesArg = selectListImagesQueryArgs(getState());
const listImagesArg = selectListImagesBaseQueryArgs(getState());
for (const imageDTO of imageDTOs) {
if (imageDTO.is_intermediate) {
@@ -94,7 +94,7 @@ export const buildOnInvocationComplete = (getState: AppGetState, dispatch: AppDi
type: 'ImageList' as const,
id: imageListId,
}));
dispatch(imagesApi.util.invalidateTags([...boardTags, ...imageListTags]));
dispatch(imagesApi.util.invalidateTags(['ImageNameList', ...boardTags, ...imageListTags]));
const autoSwitch = selectAutoSwitch(getState());