Skip to content

Commit 0724eb9

Browse files
psychedeliciousMary Hipp
andauthored
feat(ui): another go at gallery (#3791)
* feat(ui): migrate listImages to RTK query using createEntityAdapter - see comments in `endpoints/images.ts` for explanation of the caching - so far, only manually updating `all` images when new image is generated. no other manual cache updates are implemented, but will be needed. - fixed some weirdness with loading state components (like the spinners in gallery) - added `useThumbnailFallback` for `IAIDndImage`, this displays the tiny webp thumbnail while the full-size images load - comment out some old thunk related stuff in gallerySlice, which is no longer needed * feat(ui): add manual cache updates for board changes (wip) - update RTK Query caches when adding/removing single image to/from board - work more on migrating all image-related operations to RTK Query * update AddImagesToBoardContext so that it works when user uses context menu + modal * handle case where no image is selected * get assets working for main list and boards - dnd only * feat(ui): migrate image uploads to RTK Query - minor refactor of `ImageUploader` and `useImageUploadButton` hooks, simplify some logic - style filesystem upload overlay to match existing UI - replace all old `imageUploaded` thunks with `uploadImage` RTK Query calls, update associated logic including canvas related uploads - simplify `PostUploadAction`s that only need to display user input * feat(ui): remove `receivedPageOfImages` thunks * feat(ui): remove `receivedImageUrls` thunk * feat(ui): finish removing all images thunks stuff now broken: - image usage - delete board images - on first load, no image selected * feat(ui): simplify `updateImage` cache manipulation - we don't actually ever change categories, so we can remove a lot of logic * feat(ui): simplify canvas autosave - instead of using a network request to set the canvas generation as not intermediate, we can just do that in the graph * feat(ui): simplify & handle edge cases in cache updates * feat(db, api): support `board_id='none'` for `get_many` images queries This allows us to get all images that are not on a board. * chore(ui): regen types * feat(ui): add `All Assets`, `No Board` boards Restructure boards: - `all images` is all images - `all assets` is all assets - `no board` is all images/assets without a board set - user boards may have images and assets Update caching logic - much simpler without every board having sub-views of images and assets - update drag and drop operations for all possible interactions * chore(ui): regen types * feat(ui): move download to top of context menu * feat(ui): improve drop overlay styles * fix(ui): fix image not selected on first load - listen for first load of all images board, then select the first image * feat(ui): refactor board deletion api changes: - add route to list all image names for a board. this is required to handle board + image deletion. we need to know every image in the board to determine the image usage across the app. this is fetched only when the delete board and images modal is opened so it's as efficient as it can be. - update the delete board route to respond with a list of deleted `board_images` and `images`, as image names. this is needed to perform accurate clientside state & cache updates after deleting. db changes: - remove unused `board_images` service method to get paginated images dtos for a board. this is now done thru the list images endpoint & images service. needs a small logic change on `images.delete_images_on_board` ui changes: - simplify the delete board modal - no context, just minor prop drilling. this is feasible for boards only because the components that need to trigger and manipulate the modal are very close together in the tree - add cache updates for `deleteBoard` & `deleteBoardAndImages` mutations - the only thing we cannot do directly is on `deleteBoardAndImages`, update the `No Board` board. we'd need to insert image dtos that we may not have loaded. instead, i am just invalidating the tags for that `listImages` cache. so when you `deleteBoardAndImages`, the `No Board` will re-fetch the initial image limit. i think this is more efficient than e.g. fetching all image dtos to insert then inserting them. - handle image usage for `deleteBoardAndImages` - update all (i think/hope) the little bits and pieces in the UI to accomodate these changes * fix(ui): fix board selection logic * feat(ui): add delete board modal loading state * fix(ui): use thumbnails for board cover images * fix(ui): fix race condition with board selection when selecting a board that doesn't have any images loaded, we need to wait until the images haveloaded before selecting the first image. this logic is debounced to ~1000ms. * feat(ui): name 'No Board' correctly, change icon * fix(ui): do not cache listAllImageNames query if we cache it, we can end up with stale image usage during deletion. we could of course manually update the cache as we are doing elsewhere. but because this is a relatively infrequent network request, i'd like to trade increased cache mgmt complexity here for increased resource usage. * feat(ui): reduce drag preview opacity, remove border * fix(ui): fix incorrect queryArg used in `deleteImage` and `updateImage` cache updates * fix(ui): fix doubled open in new tab * fix(ui): fix new generations not getting added to 'No Board' * fix(ui): fix board id not changing on new image when autosave enabled * fix(ui): context menu when selection is 0 need to revise how context menu is triggered later, when we approach multi select * fix(ui): fix deleting does not update counts for all images and all assets * fix(ui): fix all assets board name in boards list collapse button * fix(ui): ensure we never go under 0 for total board count * fix(ui): fix text overflow on board names --------- Co-authored-by: Mary Hipp <[email protected]>
1 parent 055f5b2 commit 0724eb9

File tree

100 files changed

+3060
-2815
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

100 files changed

+3060
-2815
lines changed

invokeai/app/api/routers/board_images.py

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,14 @@ async def create_board_image(
2424
):
2525
"""Creates a board_image"""
2626
try:
27-
result = ApiDependencies.invoker.services.board_images.add_image_to_board(board_id=board_id, image_name=image_name)
27+
result = ApiDependencies.invoker.services.board_images.add_image_to_board(
28+
board_id=board_id, image_name=image_name
29+
)
2830
return result
2931
except Exception as e:
3032
raise HTTPException(status_code=500, detail="Failed to add to board")
31-
33+
34+
3235
@board_images_router.delete(
3336
"/",
3437
operation_id="remove_board_image",
@@ -43,27 +46,10 @@ async def remove_board_image(
4346
):
4447
"""Deletes a board_image"""
4548
try:
46-
result = ApiDependencies.invoker.services.board_images.remove_image_from_board(board_id=board_id, image_name=image_name)
49+
result = ApiDependencies.invoker.services.board_images.remove_image_from_board(
50+
board_id=board_id, image_name=image_name
51+
)
4752
return result
4853
except Exception as e:
4954
raise HTTPException(status_code=500, detail="Failed to update board")
5055

51-
52-
53-
@board_images_router.get(
54-
"/{board_id}",
55-
operation_id="list_board_images",
56-
response_model=OffsetPaginatedResults[ImageDTO],
57-
)
58-
async def list_board_images(
59-
board_id: str = Path(description="The id of the board"),
60-
offset: int = Query(default=0, description="The page offset"),
61-
limit: int = Query(default=10, description="The number of boards per page"),
62-
) -> OffsetPaginatedResults[ImageDTO]:
63-
"""Gets a list of images for a board"""
64-
65-
results = ApiDependencies.invoker.services.board_images.get_images_for_board(
66-
board_id,
67-
)
68-
return results
69-

invokeai/app/api/routers/boards.py

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,28 @@
11
from typing import Optional, Union
2+
23
from fastapi import Body, HTTPException, Path, Query
34
from fastapi.routing import APIRouter
5+
from pydantic import BaseModel, Field
6+
47
from invokeai.app.services.board_record_storage import BoardChanges
58
from invokeai.app.services.image_record_storage import OffsetPaginatedResults
69
from invokeai.app.services.models.board_record import BoardDTO
710

8-
911
from ..dependencies import ApiDependencies
1012

1113
boards_router = APIRouter(prefix="/v1/boards", tags=["boards"])
1214

1315

16+
class DeleteBoardResult(BaseModel):
17+
board_id: str = Field(description="The id of the board that was deleted.")
18+
deleted_board_images: list[str] = Field(
19+
description="The image names of the board-images relationships that were deleted."
20+
)
21+
deleted_images: list[str] = Field(
22+
description="The names of the images that were deleted."
23+
)
24+
25+
1426
@boards_router.post(
1527
"/",
1628
operation_id="create_board",
@@ -69,25 +81,42 @@ async def update_board(
6981
raise HTTPException(status_code=500, detail="Failed to update board")
7082

7183

72-
@boards_router.delete("/{board_id}", operation_id="delete_board")
84+
@boards_router.delete(
85+
"/{board_id}", operation_id="delete_board", response_model=DeleteBoardResult
86+
)
7387
async def delete_board(
7488
board_id: str = Path(description="The id of board to delete"),
7589
include_images: Optional[bool] = Query(
7690
description="Permanently delete all images on the board", default=False
7791
),
78-
) -> None:
92+
) -> DeleteBoardResult:
7993
"""Deletes a board"""
8094
try:
8195
if include_images is True:
96+
deleted_images = ApiDependencies.invoker.services.board_images.get_all_board_image_names_for_board(
97+
board_id=board_id
98+
)
8299
ApiDependencies.invoker.services.images.delete_images_on_board(
83100
board_id=board_id
84101
)
85102
ApiDependencies.invoker.services.boards.delete(board_id=board_id)
103+
return DeleteBoardResult(
104+
board_id=board_id,
105+
deleted_board_images=[],
106+
deleted_images=deleted_images,
107+
)
86108
else:
109+
deleted_board_images = ApiDependencies.invoker.services.board_images.get_all_board_image_names_for_board(
110+
board_id=board_id
111+
)
87112
ApiDependencies.invoker.services.boards.delete(board_id=board_id)
113+
return DeleteBoardResult(
114+
board_id=board_id,
115+
deleted_board_images=deleted_board_images,
116+
deleted_images=[],
117+
)
88118
except Exception as e:
89-
# TODO: Does this need any exception handling at all?
90-
pass
119+
raise HTTPException(status_code=500, detail="Failed to delete board")
91120

92121

93122
@boards_router.get(
@@ -115,3 +144,19 @@ async def list_boards(
115144
status_code=400,
116145
detail="Invalid request: Must provide either 'all' or both 'offset' and 'limit'",
117146
)
147+
148+
149+
@boards_router.get(
150+
"/{board_id}/image_names",
151+
operation_id="list_all_board_image_names",
152+
response_model=list[str],
153+
)
154+
async def list_all_board_image_names(
155+
board_id: str = Path(description="The id of the board"),
156+
) -> list[str]:
157+
"""Gets a list of images for a board"""
158+
159+
image_names = ApiDependencies.invoker.services.board_images.get_all_board_image_names_for_board(
160+
board_id,
161+
)
162+
return image_names

invokeai/app/api/routers/images.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -245,16 +245,16 @@ async def get_image_urls(
245245
)
246246
async def list_image_dtos(
247247
image_origin: Optional[ResourceOrigin] = Query(
248-
default=None, description="The origin of images to list"
248+
default=None, description="The origin of images to list."
249249
),
250250
categories: Optional[list[ImageCategory]] = Query(
251-
default=None, description="The categories of image to include"
251+
default=None, description="The categories of image to include."
252252
),
253253
is_intermediate: Optional[bool] = Query(
254-
default=None, description="Whether to list intermediate images"
254+
default=None, description="Whether to list intermediate images."
255255
),
256256
board_id: Optional[str] = Query(
257-
default=None, description="The board id to filter by"
257+
default=None, description="The board id to filter by. Use 'none' to find images without a board."
258258
),
259259
offset: int = Query(default=0, description="The page offset"),
260260
limit: int = Query(default=10, description="The number of images per page"),

invokeai/app/services/board_image_record_storage.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ def remove_image_from_board(
3232
pass
3333

3434
@abstractmethod
35-
def get_images_for_board(
35+
def get_all_board_image_names_for_board(
3636
self,
3737
board_id: str,
38-
) -> OffsetPaginatedResults[ImageRecord]:
39-
"""Gets images for a board."""
38+
) -> list[str]:
39+
"""Gets all board images for a board, as a list of the image names."""
4040
pass
4141

4242
@abstractmethod
@@ -211,6 +211,26 @@ def get_images_for_board(
211211
items=images, offset=offset, limit=limit, total=count
212212
)
213213

214+
def get_all_board_image_names_for_board(self, board_id: str) -> list[str]:
215+
try:
216+
self._lock.acquire()
217+
self._cursor.execute(
218+
"""--sql
219+
SELECT image_name
220+
FROM board_images
221+
WHERE board_id = ?;
222+
""",
223+
(board_id,),
224+
)
225+
result = cast(list[sqlite3.Row], self._cursor.fetchall())
226+
image_names = list(map(lambda r: r[0], result))
227+
return image_names
228+
except sqlite3.Error as e:
229+
self._conn.rollback()
230+
raise e
231+
finally:
232+
self._lock.release()
233+
214234
def get_board_for_image(
215235
self,
216236
image_name: str,

invokeai/app/services/board_images.py

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ def remove_image_from_board(
3838
pass
3939

4040
@abstractmethod
41-
def get_images_for_board(
41+
def get_all_board_image_names_for_board(
4242
self,
4343
board_id: str,
44-
) -> OffsetPaginatedResults[ImageDTO]:
45-
"""Gets images for a board."""
44+
) -> list[str]:
45+
"""Gets all board images for a board, as a list of the image names."""
4646
pass
4747

4848
@abstractmethod
@@ -98,30 +98,13 @@ def remove_image_from_board(
9898
) -> None:
9999
self._services.board_image_records.remove_image_from_board(board_id, image_name)
100100

101-
def get_images_for_board(
101+
def get_all_board_image_names_for_board(
102102
self,
103103
board_id: str,
104-
) -> OffsetPaginatedResults[ImageDTO]:
105-
image_records = self._services.board_image_records.get_images_for_board(
104+
) -> list[str]:
105+
return self._services.board_image_records.get_all_board_image_names_for_board(
106106
board_id
107107
)
108-
image_dtos = list(
109-
map(
110-
lambda r: image_record_to_dto(
111-
r,
112-
self._services.urls.get_image_url(r.image_name),
113-
self._services.urls.get_image_url(r.image_name, True),
114-
board_id,
115-
),
116-
image_records.items,
117-
)
118-
)
119-
return OffsetPaginatedResults[ImageDTO](
120-
items=image_dtos,
121-
offset=image_records.offset,
122-
limit=image_records.limit,
123-
total=image_records.total,
124-
)
125108

126109
def get_board_for_image(
127110
self,
@@ -136,7 +119,7 @@ def board_record_to_dto(
136119
) -> BoardDTO:
137120
"""Converts a board record to a board DTO."""
138121
return BoardDTO(
139-
**board_record.dict(exclude={'cover_image_name'}),
122+
**board_record.dict(exclude={"cover_image_name"}),
140123
cover_image_name=cover_image_name,
141124
image_count=image_count,
142125
)

invokeai/app/services/image_record_storage.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010

1111
from invokeai.app.models.image import ImageCategory, ResourceOrigin
1212
from invokeai.app.services.models.image_record import (
13-
ImageRecord, ImageRecordChanges, deserialize_image_record)
13+
ImageRecord,
14+
ImageRecordChanges,
15+
deserialize_image_record,
16+
)
1417

1518
T = TypeVar("T", bound=BaseModel)
1619

@@ -377,11 +380,15 @@ def get_many(
377380

378381
query_params.append(is_intermediate)
379382

380-
if board_id is not None:
383+
# board_id of "none" is reserved for images without a board
384+
if board_id == "none":
385+
query_conditions += """--sql
386+
AND board_images.board_id IS NULL
387+
"""
388+
elif board_id is not None:
381389
query_conditions += """--sql
382390
AND board_images.board_id = ?
383391
"""
384-
385392
query_params.append(board_id)
386393

387394
query_pagination = """--sql

invokeai/app/services/images.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
InvalidOriginException, ResourceOrigin)
1212
from invokeai.app.services.board_image_record_storage import \
1313
BoardImageRecordStorageBase
14-
from invokeai.app.services.graph import Graph
1514
from invokeai.app.services.image_file_storage import (
1615
ImageFileDeleteException, ImageFileNotFoundException,
1716
ImageFileSaveException, ImageFileStorageBase)
@@ -385,16 +384,14 @@ def delete(self, image_name: str):
385384

386385
def delete_images_on_board(self, board_id: str):
387386
try:
388-
images = self._services.board_image_records.get_images_for_board(board_id)
389-
image_name_list = list(
390-
map(
391-
lambda r: r.image_name,
392-
images.items,
387+
image_names = (
388+
self._services.board_image_records.get_all_board_image_names_for_board(
389+
board_id
393390
)
394391
)
395-
for image_name in image_name_list:
392+
for image_name in image_names:
396393
self._services.image_files.delete(image_name)
397-
self._services.image_records.delete_many(image_name_list)
394+
self._services.image_records.delete_many(image_names)
398395
except ImageRecordDeleteException:
399396
self._services.logger.error(f"Failed to delete image records")
400397
raise

invokeai/frontend/web/src/app/components/App.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import InvokeTabs from 'features/ui/components/InvokeTabs';
1515
import ParametersDrawer from 'features/ui/components/ParametersDrawer';
1616
import i18n from 'i18n';
1717
import { ReactNode, memo, useEffect } from 'react';
18-
import DeleteBoardImagesModal from '../../features/gallery/components/Boards/DeleteBoardImagesModal';
1918
import UpdateImageBoardModal from '../../features/gallery/components/Boards/UpdateImageBoardModal';
2019
import GlobalHotkeys from './GlobalHotkeys';
2120
import Toaster from './Toaster';
@@ -84,7 +83,6 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
8483
</Grid>
8584
<DeleteImageModal />
8685
<UpdateImageBoardModal />
87-
<DeleteBoardImagesModal />
8886
<Toaster />
8987
<GlobalHotkeys />
9088
</>

invokeai/frontend/web/src/app/components/ImageDnd/DragPreview.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,7 @@ const STYLES: ChakraProps['sx'] = {
1515
maxH: BOX_SIZE,
1616
shadow: 'dark-lg',
1717
borderRadius: 'lg',
18-
borderWidth: 2,
19-
borderStyle: 'dashed',
20-
borderColor: 'base.100',
21-
opacity: 0.5,
18+
opacity: 0.3,
2219
bg: 'base.800',
2320
color: 'base.50',
2421
_dark: {

invokeai/frontend/web/src/app/components/ImageDnd/ImageDndContext.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const ImageDndContext = (props: ImageDndContextProps) => {
2828
const dispatch = useAppDispatch();
2929

3030
const handleDragStart = useCallback((event: DragStartEvent) => {
31+
console.log('dragStart', event.active.data.current);
3132
const activeData = event.active.data.current;
3233
if (!activeData) {
3334
return;
@@ -37,15 +38,16 @@ const ImageDndContext = (props: ImageDndContextProps) => {
3738

3839
const handleDragEnd = useCallback(
3940
(event: DragEndEvent) => {
41+
console.log('dragEnd', event.active.data.current);
4042
const activeData = event.active.data.current;
4143
const overData = event.over?.data.current;
42-
if (!activeData || !overData) {
44+
if (!activeDragData || !overData) {
4345
return;
4446
}
45-
dispatch(dndDropped({ overData, activeData }));
47+
dispatch(dndDropped({ overData, activeData: activeDragData }));
4648
setActiveDragData(null);
4749
},
48-
[dispatch]
50+
[activeDragData, dispatch]
4951
);
5052

5153
const mouseSensor = useSensor(MouseSensor, {

0 commit comments

Comments
 (0)