feat(ui): revise bulk download listeners

- Use a single listener for all of the to keep them in one spot
- Use the bulk download item name as a toast id so we can update the existing toasts
- Update handling to work with other environments
- Move all bulk download handling from components to listener
This commit is contained in:
psychedelicious 2024-02-20 22:26:38 +11:00 committed by Brandon Rising
parent ffef5c65bb
commit 85dae6ad1e
7 changed files with 131 additions and 140 deletions

View File

@ -424,10 +424,11 @@
"uploads": "Uploads",
"deleteSelection": "Delete Selection",
"downloadSelection": "Download Selection",
"preparingDownload": "Preparing Download",
"preparingDownloadFailed": "Problem Preparing Download",
"bulkDownloadStarting": "Beginning Download",
"bulkDownloadFailed": "Problem Preparing Download",
"bulkDownloadRequested": "Preparing Download",
"bulkDownloadRequestedDesc": "Your download request is being prepared. This may take a few moments.",
"bulkDownloadRequestFailed": "Problem Preparing Download",
"bulkDownloadStarting": "Download Starting",
"bulkDownloadFailed": "Download Failed",
"problemDeletingImages": "Problem Deleting Images",
"problemDeletingImagesDesc": "One or more images could not be deleted"
},

View File

@ -1,5 +1,6 @@
import type { ListenerEffect, TypedAddListener, TypedStartListening, UnknownAction } from '@reduxjs/toolkit';
import { addListener, createListenerMiddleware } from '@reduxjs/toolkit';
import { addBulkDownloadListeners } from 'app/store/middleware/listenerMiddleware/listeners/bulkDownload';
import { addGalleryImageClickedListener } from 'app/store/middleware/listenerMiddleware/listeners/galleryImageClicked';
import type { AppDispatch, RootState } from 'app/store/store';
@ -48,8 +49,6 @@ import { addInitialImageSelectedListener } from './listeners/initialImageSelecte
import { addModelSelectedListener } from './listeners/modelSelected';
import { addModelsLoadedListener } from './listeners/modelsLoaded';
import { addDynamicPromptsListener } from './listeners/promptChanged';
import { addBulkDownloadCompleteEventListener } from './listeners/socketio/socketBulkDownloadComplete';
import { addBulkDownloadFailedEventListener } from './listeners/socketio/socketBulkDownloadFailed';
import { addSocketConnectedEventListener as addSocketConnectedListener } from './listeners/socketio/socketConnected';
import { addSocketDisconnectedEventListener as addSocketDisconnectedListener } from './listeners/socketio/socketDisconnected';
import { addGeneratorProgressEventListener as addGeneratorProgressListener } from './listeners/socketio/socketGeneratorProgress';
@ -139,8 +138,7 @@ addModelLoadEventListener();
addSessionRetrievalErrorEventListener();
addInvocationRetrievalErrorEventListener();
addSocketQueueItemStatusChangedEventListener();
addBulkDownloadCompleteEventListener();
addBulkDownloadFailedEventListener();
addBulkDownloadListeners();
// ControlNet
addControlNetImageProcessedListener();

View File

@ -0,0 +1,118 @@
import type { UseToastOptions } from '@invoke-ai/ui-library';
import { createStandaloneToast, theme, TOAST_OPTIONS } from '@invoke-ai/ui-library';
import { logger } from 'app/logging/logger';
import { startAppListening } from 'app/store/middleware/listenerMiddleware';
import { t } from 'i18next';
import { imagesApi } from 'services/api/endpoints/images';
import {
socketBulkDownloadCompleted,
socketBulkDownloadFailed,
socketBulkDownloadStarted,
} from 'services/events/actions';
const log = logger('images');
const { toast } = createStandaloneToast({
theme: theme,
defaultOptions: TOAST_OPTIONS.defaultOptions,
});
export const addBulkDownloadListeners = () => {
startAppListening({
matcher: imagesApi.endpoints.bulkDownloadImages.matchFulfilled,
effect: async (action) => {
log.debug(action.payload, 'Bulk download requested');
// If we have an item name, we are processing the bulk download locally and should use it as the toast id to
// prevent multiple toasts for the same item.
toast({
id: action.payload.bulk_download_item_name ?? undefined,
title: t('gallery.bulkDownloadRequested'),
status: 'success',
// Show the response message if it exists, otherwise show the default message
description: action.payload.response || t('gallery.bulkDownloadRequestedDesc'),
duration: null,
isClosable: true,
});
},
});
startAppListening({
matcher: imagesApi.endpoints.bulkDownloadImages.matchRejected,
effect: async () => {
log.debug('Bulk download request failed');
// There isn't any toast to update if we get this event.
toast({
title: t('gallery.bulkDownloadRequestFailed'),
status: 'success',
isClosable: true,
});
},
});
startAppListening({
actionCreator: socketBulkDownloadStarted,
effect: async (action) => {
// This should always happen immediately after the bulk download request, so we don't need to show a toast here.
log.debug(action.payload.data, 'Bulk download preparation started');
},
});
startAppListening({
actionCreator: socketBulkDownloadCompleted,
effect: async (action) => {
log.debug(action.payload.data, 'Bulk download preparation completed');
const { bulk_download_item_name } = action.payload.data;
// TODO(psyche): This URL may break in in some environments (e.g. Nvidia workbench) but we need to test it first
const url = `/api/v1/images/download/${bulk_download_item_name}`;
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = bulk_download_item_name;
document.body.appendChild(a);
a.click();
const toastOptions: UseToastOptions = {
id: bulk_download_item_name,
title: t('gallery.bulkDownloadStarting'),
status: 'success',
description: bulk_download_item_name,
duration: 5000,
isClosable: true,
};
if (toast.isActive(bulk_download_item_name)) {
toast.update(bulk_download_item_name, toastOptions);
} else {
toast(toastOptions);
}
},
});
startAppListening({
actionCreator: socketBulkDownloadFailed,
effect: async (action) => {
log.debug(action.payload.data, 'Bulk download preparation failed');
const { bulk_download_item_name } = action.payload.data;
const toastOptions: UseToastOptions = {
id: bulk_download_item_name,
title: t('gallery.bulkDownloadFailed'),
status: 'error',
description: action.payload.data.error,
duration: null,
isClosable: true,
};
if (toast.isActive(bulk_download_item_name)) {
toast.update(bulk_download_item_name, toastOptions);
} else {
toast(toastOptions);
}
},
});
};

View File

@ -1,41 +0,0 @@
import { logger } from 'app/logging/logger';
import { addToast } from 'features/system/store/systemSlice';
import { t } from 'i18next';
import { socketBulkDownloadCompleted } from 'services/events/actions';
import { startAppListening } from '../..';
const log = logger('socketio');
export const addBulkDownloadCompleteEventListener = () => {
startAppListening({
actionCreator: socketBulkDownloadCompleted,
effect: async (action, { dispatch }) => {
log.debug(action.payload, 'Bulk download complete');
const bulk_download_item_name = action.payload.data.bulk_download_item_name;
const url = `/api/v1/images/download/${bulk_download_item_name}`;
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = bulk_download_item_name;
document.body.appendChild(a);
a.click();
dispatch(
addToast({
title: t('gallery.bulkDownloadStarting'),
status: 'success',
...(action.payload
? {
description: bulk_download_item_name,
duration: null,
isClosable: true,
}
: {}),
})
);
},
});
};

View File

@ -1,32 +0,0 @@
import { logger } from 'app/logging/logger';
import { addToast } from 'features/system/store/systemSlice';
import { t } from 'i18next';
import { socketBulkDownloadFailed } from 'services/events/actions';
import { startAppListening } from '../..';
const log = logger('socketio');
export const addBulkDownloadFailedEventListener = () => {
startAppListening({
actionCreator: socketBulkDownloadFailed,
effect: async (action, { dispatch }) => {
log.debug(action.payload, 'Bulk download error');
dispatch(
addToast({
title: t('gallery.bulkDownloadFailed'),
status: 'error',
...(action.payload
? {
description: action.payload.data.error,
duration: null,
isClosable: true,
}
: {}),
})
);
},
});
};

View File

@ -5,7 +5,6 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { autoAddBoardIdChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
import type { BoardId } from 'features/gallery/store/types';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { addToast } from 'features/system/store/systemSlice';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiDownloadBold, PiPlusBold } from 'react-icons/pi';
@ -41,35 +40,9 @@ const BoardContextMenu = ({ board, board_id, setBoardToDelete, children }: Props
dispatch(autoAddBoardIdChanged(board_id));
}, [board_id, dispatch]);
const handleBulkDownload = useCallback(async () => {
try {
const response = await bulkDownload({
image_names: [],
board_id: board_id,
}).unwrap();
dispatch(
addToast({
title: t('gallery.preparingDownload'),
status: 'success',
...(response.response
? {
description: response.response,
duration: null,
isClosable: true,
}
: {}),
})
);
} catch {
dispatch(
addToast({
title: t('gallery.preparingDownloadFailed'),
status: 'error',
})
);
}
}, [t, board_id, bulkDownload, dispatch]);
const handleBulkDownload = useCallback(() => {
bulkDownload({ image_names: [], board_id: board_id });
}, [board_id, bulkDownload]);
const renderMenuFunc = useCallback(
() => (

View File

@ -5,7 +5,6 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { imagesToChangeSelected, isModalOpenChanged } from 'features/changeBoardModal/store/slice';
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { addToast } from 'features/system/store/systemSlice';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiDownloadSimpleBold, PiFoldersBold, PiStarBold, PiStarFill, PiTrashSimpleBold } from 'react-icons/pi';
@ -44,34 +43,9 @@ const MultipleSelectionMenuItems = () => {
unstarImages({ imageDTOs: selection });
}, [unstarImages, selection]);
const handleBulkDownload = useCallback(async () => {
try {
const response = await bulkDownload({
image_names: selection.map((img) => img.image_name),
}).unwrap();
dispatch(
addToast({
title: t('gallery.preparingDownload'),
status: 'success',
...(response.response
? {
description: response.response,
duration: null,
isClosable: true,
}
: {}),
})
);
} catch {
dispatch(
addToast({
title: t('gallery.preparingDownloadFailed'),
status: 'error',
})
);
}
}, [t, selection, bulkDownload, dispatch]);
const handleBulkDownload = useCallback(() => {
bulkDownload({ image_names: selection.map((img) => img.image_name) });
}, [selection, bulkDownload]);
const areAllStarred = useMemo(() => {
return selection.every((img) => img.starred);