mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-17 17:37:55 -05:00
Compare commits
96 Commits
main
...
maryhipp/v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2faa3e72a3 | ||
|
|
e670866585 | ||
|
|
afa43053c4 | ||
|
|
8cb3f31fe1 | ||
|
|
0875674ad5 | ||
|
|
f7a4aceb1f | ||
|
|
a123a7f29f | ||
|
|
2fb4275b3c | ||
|
|
56f2b7d9e9 | ||
|
|
a155ce5954 | ||
|
|
fc4e1366ed | ||
|
|
4416301395 | ||
|
|
eb6087eab0 | ||
|
|
fc3c789df3 | ||
|
|
6ec1d2e368 | ||
|
|
094c0c15ed | ||
|
|
108b55d7f2 | ||
|
|
5765fce6b9 | ||
|
|
9285f24c39 | ||
|
|
97b6abf50f | ||
|
|
8951bdeef3 | ||
|
|
9a7a66cf5a | ||
|
|
b39a16aa84 | ||
|
|
1dd79bddb3 | ||
|
|
45bfaf1f39 | ||
|
|
1100b3eb4f | ||
|
|
1e0bed9a89 | ||
|
|
09fb5ab04f | ||
|
|
cc2b0ae8c1 | ||
|
|
f3690bec7d | ||
|
|
4ebaa8982a | ||
|
|
7d98dd31af | ||
|
|
bd4af9c04c | ||
|
|
201769711b | ||
|
|
f5ac1df7d2 | ||
|
|
f8c940bc11 | ||
|
|
e818027dc6 | ||
|
|
5c80a2a8ab | ||
|
|
d6a3e7b7a4 | ||
|
|
01563eab3b | ||
|
|
438658c89b | ||
|
|
f5f4527b36 | ||
|
|
b4b54d4d2a | ||
|
|
32e2d176de | ||
|
|
2b688ed855 | ||
|
|
b52a885e74 | ||
|
|
38b43228d6 | ||
|
|
447754318b | ||
|
|
160a9b78cb | ||
|
|
770b1a9f7a | ||
|
|
550c32c2cd | ||
|
|
e46842bea2 | ||
|
|
cda78f3c01 | ||
|
|
e75bc159bd | ||
|
|
b5b75f3e8d | ||
|
|
42ab6890f5 | ||
|
|
2d2ba257c6 | ||
|
|
f1d15c9a3d | ||
|
|
9937e35b02 | ||
|
|
7d5b49e70a | ||
|
|
1cc9583335 | ||
|
|
517cd0880e | ||
|
|
3dcc290498 | ||
|
|
2e3ece2f02 | ||
|
|
b7a141d13c | ||
|
|
8a8705c10b | ||
|
|
7714c3234b | ||
|
|
d70d9cf88b | ||
|
|
0dddd02107 | ||
|
|
f9c55c2773 | ||
|
|
dc652b68c7 | ||
|
|
bd2009681d | ||
|
|
7ea5bd5344 | ||
|
|
5c9c8f0110 | ||
|
|
d611454a77 | ||
|
|
fe008439d4 | ||
|
|
3098ae7259 | ||
|
|
4094f1f5e8 | ||
|
|
f871ac0554 | ||
|
|
bb71b21d15 | ||
|
|
a170537c27 | ||
|
|
dba78743a4 | ||
|
|
65b3d5ea15 | ||
|
|
5651bbe7c9 | ||
|
|
e393bc5d6c | ||
|
|
b1ac69fd35 | ||
|
|
66d8b86149 | ||
|
|
d0e12c31f7 | ||
|
|
7fb396d86e | ||
|
|
26f94e6ddf | ||
|
|
d6651ba9a7 | ||
|
|
af8ad1de3d | ||
|
|
efb54151fd | ||
|
|
8d5e2b0816 | ||
|
|
0224fab631 | ||
|
|
e6ef3a78d6 |
55
getItemsPerRow.ts
Normal file
55
getItemsPerRow.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Calculate how many images fit in a row based on the current grid layout.
|
||||
*
|
||||
* TODO(psyche): We only need to do this when the gallery width changes, or when the galleryImageMinimumWidth value
|
||||
* changes. Cache this calculation.
|
||||
*/
|
||||
export const getItemsPerRow = (rootEl: HTMLDivElement): number => {
|
||||
// Start from root and find virtuoso grid elements
|
||||
const gridElement = rootEl.querySelector('.virtuoso-grid-list');
|
||||
|
||||
if (!gridElement) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const firstGridItem = gridElement.querySelector('.virtuoso-grid-item');
|
||||
|
||||
if (!firstGridItem) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const itemRect = firstGridItem.getBoundingClientRect();
|
||||
const containerRect = gridElement.getBoundingClientRect();
|
||||
|
||||
// Get the computed gap from CSS
|
||||
const gridStyle = window.getComputedStyle(gridElement);
|
||||
const gapValue = gridStyle.gap;
|
||||
const gap = parseFloat(gapValue);
|
||||
|
||||
if (isNaN(gap) || !itemRect.width || !itemRect.height || !containerRect.width || !containerRect.height) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* You might be tempted to just do some simple math like:
|
||||
* const itemsPerRow = Math.floor(containerRect.width / itemRect.width);
|
||||
*
|
||||
* But floating point precision can cause issues with this approach, causing it to be off by 1 in some cases.
|
||||
*
|
||||
* Instead, we use a more robust approach that iteratively calculates how many items fit in the row.
|
||||
*/
|
||||
let itemsPerRow = 0;
|
||||
let spaceUsed = 0;
|
||||
|
||||
// Floating point precision can cause imagesPerRow to be 1 too small. Adding 1px to the container size fixes
|
||||
// this, without the possibility of accidentally adding an extra column.
|
||||
while (spaceUsed + itemRect.width <= containerRect.width + 1) {
|
||||
itemsPerRow++; // Increment the number of items
|
||||
spaceUsed += itemRect.width; // Add image size to the used space
|
||||
if (spaceUsed + gap <= containerRect.width) {
|
||||
spaceUsed += gap; // Add gap size to the used space after each item except after the last item
|
||||
}
|
||||
}
|
||||
|
||||
return Math.max(1, itemsPerRow);
|
||||
};
|
||||
39
invokeai/app/api/routers/board_videos.py
Normal file
39
invokeai/app/api/routers/board_videos.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from fastapi import Body, HTTPException
|
||||
from fastapi.routing import APIRouter
|
||||
|
||||
from invokeai.app.services.videos_common import AddVideosToBoardResult, RemoveVideosFromBoardResult
|
||||
|
||||
board_videos_router = APIRouter(prefix="/v1/board_videos", tags=["boards"])
|
||||
|
||||
|
||||
@board_videos_router.post(
|
||||
"/batch",
|
||||
operation_id="add_videos_to_board",
|
||||
responses={
|
||||
201: {"description": "Videos were added to board successfully"},
|
||||
},
|
||||
status_code=201,
|
||||
response_model=AddVideosToBoardResult,
|
||||
)
|
||||
async def add_videos_to_board(
|
||||
board_id: str = Body(description="The id of the board to add to"),
|
||||
video_ids: list[str] = Body(description="The ids of the videos to add", embed=True),
|
||||
) -> AddVideosToBoardResult:
|
||||
"""Adds a list of videos to a board"""
|
||||
raise HTTPException(status_code=501, detail="Not implemented")
|
||||
|
||||
|
||||
@board_videos_router.post(
|
||||
"/batch/delete",
|
||||
operation_id="remove_videos_from_board",
|
||||
responses={
|
||||
201: {"description": "Videos were removed from board successfully"},
|
||||
},
|
||||
status_code=201,
|
||||
response_model=RemoveVideosFromBoardResult,
|
||||
)
|
||||
async def remove_videos_from_board(
|
||||
video_ids: list[str] = Body(description="The ids of the videos to remove", embed=True),
|
||||
) -> RemoveVideosFromBoardResult:
|
||||
"""Removes a list of videos from their board, if they had one"""
|
||||
raise HTTPException(status_code=501, detail="Not implemented")
|
||||
119
invokeai/app/api/routers/videos.py
Normal file
119
invokeai/app/api/routers/videos.py
Normal file
@@ -0,0 +1,119 @@
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import Body, HTTPException, Path, Query
|
||||
from fastapi.routing import APIRouter
|
||||
|
||||
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
|
||||
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
|
||||
from invokeai.app.services.videos_common import (
|
||||
DeleteVideosResult,
|
||||
StarredVideosResult,
|
||||
UnstarredVideosResult,
|
||||
VideoDTO,
|
||||
VideoIdsResult,
|
||||
VideoRecordChanges,
|
||||
)
|
||||
|
||||
videos_router = APIRouter(prefix="/v1/videos", tags=["videos"])
|
||||
|
||||
|
||||
@videos_router.patch(
|
||||
"/i/{video_id}",
|
||||
operation_id="update_video",
|
||||
response_model=VideoDTO,
|
||||
)
|
||||
async def update_video(
|
||||
video_id: str = Path(description="The id of the video to update"),
|
||||
video_changes: VideoRecordChanges = Body(description="The changes to apply to the video"),
|
||||
) -> VideoDTO:
|
||||
"""Updates a video"""
|
||||
|
||||
raise HTTPException(status_code=501, detail="Not implemented")
|
||||
|
||||
|
||||
@videos_router.get(
|
||||
"/i/{video_id}",
|
||||
operation_id="get_video_dto",
|
||||
response_model=VideoDTO,
|
||||
)
|
||||
async def get_video_dto(
|
||||
video_id: str = Path(description="The id of the video to get"),
|
||||
) -> VideoDTO:
|
||||
"""Gets a video's DTO"""
|
||||
|
||||
raise HTTPException(status_code=501, detail="Not implemented")
|
||||
|
||||
|
||||
@videos_router.post("/delete", operation_id="delete_videos_from_list", response_model=DeleteVideosResult)
|
||||
async def delete_videos_from_list(
|
||||
video_ids: list[str] = Body(description="The list of ids of videos to delete", embed=True),
|
||||
) -> DeleteVideosResult:
|
||||
raise HTTPException(status_code=501, detail="Not implemented")
|
||||
|
||||
|
||||
@videos_router.post("/star", operation_id="star_videos_in_list", response_model=StarredVideosResult)
|
||||
async def star_videos_in_list(
|
||||
video_ids: list[str] = Body(description="The list of ids of videos to star", embed=True),
|
||||
) -> StarredVideosResult:
|
||||
raise HTTPException(status_code=501, detail="Not implemented")
|
||||
|
||||
|
||||
@videos_router.post("/unstar", operation_id="unstar_videos_in_list", response_model=UnstarredVideosResult)
|
||||
async def unstar_videos_in_list(
|
||||
video_ids: list[str] = Body(description="The list of ids of videos to unstar", embed=True),
|
||||
) -> UnstarredVideosResult:
|
||||
raise HTTPException(status_code=501, detail="Not implemented")
|
||||
|
||||
|
||||
@videos_router.delete("/uncategorized", operation_id="delete_uncategorized_videos", response_model=DeleteVideosResult)
|
||||
async def delete_uncategorized_videos() -> DeleteVideosResult:
|
||||
"""Deletes all videos that are uncategorized"""
|
||||
|
||||
raise HTTPException(status_code=501, detail="Not implemented")
|
||||
|
||||
|
||||
@videos_router.get("/", operation_id="list_video_dtos", response_model=OffsetPaginatedResults[VideoDTO])
|
||||
async def list_video_dtos(
|
||||
is_intermediate: Optional[bool] = Query(default=None, description="Whether to list intermediate videos."),
|
||||
board_id: Optional[str] = Query(
|
||||
default=None,
|
||||
description="The board id to filter by. Use 'none' to find videos without a board.",
|
||||
),
|
||||
offset: int = Query(default=0, description="The page offset"),
|
||||
limit: int = Query(default=10, description="The number of videos per page"),
|
||||
order_dir: SQLiteDirection = Query(default=SQLiteDirection.Descending, description="The order of sort"),
|
||||
starred_first: bool = Query(default=True, description="Whether to sort by starred videos first"),
|
||||
search_term: Optional[str] = Query(default=None, description="The term to search for"),
|
||||
) -> OffsetPaginatedResults[VideoDTO]:
|
||||
"""Lists video DTOs"""
|
||||
|
||||
raise HTTPException(status_code=501, detail="Not implemented")
|
||||
|
||||
|
||||
@videos_router.get("/ids", operation_id="get_video_ids")
|
||||
async def get_video_ids(
|
||||
is_intermediate: Optional[bool] = Query(default=None, description="Whether to list intermediate videos."),
|
||||
board_id: Optional[str] = Query(
|
||||
default=None,
|
||||
description="The board id to filter by. Use 'none' to find videos without a board.",
|
||||
),
|
||||
order_dir: SQLiteDirection = Query(default=SQLiteDirection.Descending, description="The order of sort"),
|
||||
starred_first: bool = Query(default=True, description="Whether to sort by starred videos first"),
|
||||
search_term: Optional[str] = Query(default=None, description="The term to search for"),
|
||||
) -> VideoIdsResult:
|
||||
"""Gets ordered list of video ids with metadata for optimistic updates"""
|
||||
|
||||
raise HTTPException(status_code=501, detail="Not implemented")
|
||||
|
||||
|
||||
@videos_router.post(
|
||||
"/videos_by_ids",
|
||||
operation_id="get_videos_by_ids",
|
||||
responses={200: {"model": list[VideoDTO]}},
|
||||
)
|
||||
async def get_videos_by_ids(
|
||||
video_ids: list[str] = Body(embed=True, description="Object containing list of video ids to fetch DTOs for"),
|
||||
) -> list[VideoDTO]:
|
||||
"""Gets video DTOs for the specified video ids. Maintains order of input ids."""
|
||||
|
||||
raise HTTPException(status_code=501, detail="Not implemented")
|
||||
@@ -18,6 +18,7 @@ from invokeai.app.api.no_cache_staticfiles import NoCacheStaticFiles
|
||||
from invokeai.app.api.routers import (
|
||||
app_info,
|
||||
board_images,
|
||||
board_videos,
|
||||
boards,
|
||||
client_state,
|
||||
download_queue,
|
||||
@@ -27,6 +28,7 @@ from invokeai.app.api.routers import (
|
||||
session_queue,
|
||||
style_presets,
|
||||
utilities,
|
||||
videos,
|
||||
workflows,
|
||||
)
|
||||
from invokeai.app.api.sockets import SocketIO
|
||||
@@ -125,8 +127,10 @@ app.include_router(utilities.utilities_router, prefix="/api")
|
||||
app.include_router(model_manager.model_manager_router, prefix="/api")
|
||||
app.include_router(download_queue.download_queue_router, prefix="/api")
|
||||
app.include_router(images.images_router, prefix="/api")
|
||||
app.include_router(videos.videos_router, prefix="/api")
|
||||
app.include_router(boards.boards_router, prefix="/api")
|
||||
app.include_router(board_images.board_images_router, prefix="/api")
|
||||
app.include_router(board_videos.board_videos_router, prefix="/api")
|
||||
app.include_router(model_relationships.model_relationships_router, prefix="/api")
|
||||
app.include_router(app_info.app_router, prefix="/api")
|
||||
app.include_router(session_queue.session_queue_router, prefix="/api")
|
||||
|
||||
@@ -66,11 +66,14 @@ class UIType(str, Enum, metaclass=MetaEnum):
|
||||
ChatGPT4oModel = "ChatGPT4oModelField"
|
||||
Gemini2_5Model = "Gemini2_5ModelField"
|
||||
FluxKontextModel = "FluxKontextModelField"
|
||||
Veo3Model = "Veo3ModelField"
|
||||
RunwayModel = "RunwayModelField"
|
||||
# endregion
|
||||
|
||||
# region Misc Field Types
|
||||
Scheduler = "SchedulerField"
|
||||
Any = "AnyField"
|
||||
Video = "VideoField"
|
||||
# endregion
|
||||
|
||||
# region Internal Field Types
|
||||
@@ -225,6 +228,12 @@ class ImageField(BaseModel):
|
||||
image_name: str = Field(description="The name of the image")
|
||||
|
||||
|
||||
class VideoField(BaseModel):
|
||||
"""A video primitive field"""
|
||||
|
||||
video_id: str = Field(description="The id of the video")
|
||||
|
||||
|
||||
class BoardField(BaseModel):
|
||||
"""A board primitive field"""
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ from invokeai.app.invocations.fields import (
|
||||
SD3ConditioningField,
|
||||
TensorField,
|
||||
UIComponent,
|
||||
VideoField,
|
||||
)
|
||||
from invokeai.app.services.images.images_common import ImageDTO
|
||||
from invokeai.app.services.shared.invocation_context import InvocationContext
|
||||
@@ -287,6 +288,30 @@ class ImageCollectionInvocation(BaseInvocation):
|
||||
return ImageCollectionOutput(collection=self.collection)
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
# region Video
|
||||
|
||||
|
||||
@invocation_output("video_output")
|
||||
class VideoOutput(BaseInvocationOutput):
|
||||
"""Base class for nodes that output a video"""
|
||||
|
||||
video: VideoField = OutputField(description="The output video")
|
||||
width: int = OutputField(description="The width of the video in pixels")
|
||||
height: int = OutputField(description="The height of the video in pixels")
|
||||
duration_seconds: float = OutputField(description="The duration of the video in seconds")
|
||||
|
||||
@classmethod
|
||||
def build(cls, video_id: str, width: int, height: int, duration_seconds: float) -> "VideoOutput":
|
||||
return cls(
|
||||
video=VideoField(video_id=video_id),
|
||||
width=width,
|
||||
height=height,
|
||||
duration_seconds=duration_seconds,
|
||||
)
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
# region DenoiseMask
|
||||
|
||||
@@ -49,3 +49,11 @@ class BoardImageRecordStorageBase(ABC):
|
||||
) -> int:
|
||||
"""Gets the number of images for a board."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_asset_count_for_board(
|
||||
self,
|
||||
board_id: str,
|
||||
) -> int:
|
||||
"""Gets the number of assets for a board."""
|
||||
pass
|
||||
|
||||
@@ -3,6 +3,8 @@ from typing import Optional, cast
|
||||
|
||||
from invokeai.app.services.board_image_records.board_image_records_base import BoardImageRecordStorageBase
|
||||
from invokeai.app.services.image_records.image_records_common import (
|
||||
ASSETS_CATEGORIES,
|
||||
IMAGE_CATEGORIES,
|
||||
ImageCategory,
|
||||
ImageRecord,
|
||||
deserialize_image_record,
|
||||
@@ -151,15 +153,38 @@ class SqliteBoardImageRecordStorage(BoardImageRecordStorageBase):
|
||||
|
||||
def get_image_count_for_board(self, board_id: str) -> int:
|
||||
with self._db.transaction() as cursor:
|
||||
# Convert the enum values to unique list of strings
|
||||
category_strings = [c.value for c in set(IMAGE_CATEGORIES)]
|
||||
# Create the correct length of placeholders
|
||||
placeholders = ",".join("?" * len(category_strings))
|
||||
cursor.execute(
|
||||
"""--sql
|
||||
f"""--sql
|
||||
SELECT COUNT(*)
|
||||
FROM board_images
|
||||
INNER JOIN images ON board_images.image_name = images.image_name
|
||||
WHERE images.is_intermediate = FALSE
|
||||
WHERE images.is_intermediate = FALSE AND images.image_category IN ( {placeholders} )
|
||||
AND board_images.board_id = ?;
|
||||
""",
|
||||
(board_id,),
|
||||
(board_id),
|
||||
)
|
||||
count = cast(int, cursor.fetchone()[0])
|
||||
return count
|
||||
|
||||
def get_asset_count_for_board(self, board_id: str) -> int:
|
||||
with self._db.transaction() as cursor:
|
||||
# Convert the enum values to unique list of strings
|
||||
category_strings = [c.value for c in set(ASSETS_CATEGORIES)]
|
||||
# Create the correct length of placeholders
|
||||
placeholders = ",".join("?" * len(category_strings))
|
||||
cursor.execute(
|
||||
f"""--sql
|
||||
SELECT COUNT(*)
|
||||
FROM board_images
|
||||
INNER JOIN images ON board_images.image_name = images.image_name
|
||||
WHERE images.is_intermediate = FALSE AND images.image_category IN ( {placeholders} )
|
||||
AND board_images.board_id = ?;
|
||||
""",
|
||||
(board_id),
|
||||
)
|
||||
count = cast(int, cursor.fetchone()[0])
|
||||
return count
|
||||
|
||||
@@ -12,12 +12,20 @@ class BoardDTO(BoardRecord):
|
||||
"""The URL of the thumbnail of the most recent image in the board."""
|
||||
image_count: int = Field(description="The number of images in the board.")
|
||||
"""The number of images in the board."""
|
||||
asset_count: int = Field(description="The number of assets in the board.")
|
||||
"""The number of assets in the board."""
|
||||
video_count: int = Field(description="The number of videos in the board.")
|
||||
"""The number of videos in the board."""
|
||||
|
||||
|
||||
def board_record_to_dto(board_record: BoardRecord, cover_image_name: Optional[str], image_count: int) -> BoardDTO:
|
||||
def board_record_to_dto(
|
||||
board_record: BoardRecord, cover_image_name: Optional[str], image_count: int, asset_count: int, video_count: int
|
||||
) -> BoardDTO:
|
||||
"""Converts a board record to a board DTO."""
|
||||
return BoardDTO(
|
||||
**board_record.model_dump(exclude={"cover_image_name"}),
|
||||
cover_image_name=cover_image_name,
|
||||
image_count=image_count,
|
||||
asset_count=asset_count,
|
||||
video_count=video_count,
|
||||
)
|
||||
|
||||
@@ -17,7 +17,7 @@ class BoardService(BoardServiceABC):
|
||||
board_name: str,
|
||||
) -> BoardDTO:
|
||||
board_record = self.__invoker.services.board_records.save(board_name)
|
||||
return board_record_to_dto(board_record, None, 0)
|
||||
return board_record_to_dto(board_record, None, 0, 0, 0)
|
||||
|
||||
def get_dto(self, board_id: str) -> BoardDTO:
|
||||
board_record = self.__invoker.services.board_records.get(board_id)
|
||||
@@ -27,7 +27,9 @@ class BoardService(BoardServiceABC):
|
||||
else:
|
||||
cover_image_name = None
|
||||
image_count = self.__invoker.services.board_image_records.get_image_count_for_board(board_id)
|
||||
return board_record_to_dto(board_record, cover_image_name, image_count)
|
||||
asset_count = self.__invoker.services.board_image_records.get_asset_count_for_board(board_id)
|
||||
video_count = 0 # noop for OSS
|
||||
return board_record_to_dto(board_record, cover_image_name, image_count, asset_count, video_count)
|
||||
|
||||
def update(
|
||||
self,
|
||||
@@ -42,7 +44,9 @@ class BoardService(BoardServiceABC):
|
||||
cover_image_name = None
|
||||
|
||||
image_count = self.__invoker.services.board_image_records.get_image_count_for_board(board_id)
|
||||
return board_record_to_dto(board_record, cover_image_name, image_count)
|
||||
asset_count = self.__invoker.services.board_image_records.get_asset_count_for_board(board_id)
|
||||
video_count = 0 # noop for OSS
|
||||
return board_record_to_dto(board_record, cover_image_name, image_count, asset_count, video_count)
|
||||
|
||||
def delete(self, board_id: str) -> None:
|
||||
self.__invoker.services.board_records.delete(board_id)
|
||||
@@ -67,7 +71,9 @@ class BoardService(BoardServiceABC):
|
||||
cover_image_name = None
|
||||
|
||||
image_count = self.__invoker.services.board_image_records.get_image_count_for_board(r.board_id)
|
||||
board_dtos.append(board_record_to_dto(r, cover_image_name, image_count))
|
||||
asset_count = self.__invoker.services.board_image_records.get_asset_count_for_board(r.board_id)
|
||||
video_count = 0 # noop for OSS
|
||||
board_dtos.append(board_record_to_dto(r, cover_image_name, image_count, asset_count, video_count))
|
||||
|
||||
return OffsetPaginatedResults[BoardDTO](items=board_dtos, offset=offset, limit=limit, total=len(board_dtos))
|
||||
|
||||
@@ -84,6 +90,8 @@ class BoardService(BoardServiceABC):
|
||||
cover_image_name = None
|
||||
|
||||
image_count = self.__invoker.services.board_image_records.get_image_count_for_board(r.board_id)
|
||||
board_dtos.append(board_record_to_dto(r, cover_image_name, image_count))
|
||||
asset_count = self.__invoker.services.board_image_records.get_asset_count_for_board(r.board_id)
|
||||
video_count = 0 # noop for OSS
|
||||
board_dtos.append(board_record_to_dto(r, cover_image_name, image_count, asset_count, video_count))
|
||||
|
||||
return board_dtos
|
||||
|
||||
@@ -58,6 +58,15 @@ class ImageCategory(str, Enum, metaclass=MetaEnum):
|
||||
"""OTHER: The image is some other type of image with a specialized purpose. To be used by external nodes."""
|
||||
|
||||
|
||||
IMAGE_CATEGORIES: list[ImageCategory] = [ImageCategory.GENERAL]
|
||||
ASSETS_CATEGORIES: list[ImageCategory] = [
|
||||
ImageCategory.CONTROL,
|
||||
ImageCategory.MASK,
|
||||
ImageCategory.USER,
|
||||
ImageCategory.OTHER,
|
||||
]
|
||||
|
||||
|
||||
class InvalidImageCategoryException(ValueError):
|
||||
"""Raised when a provided value is not a valid ImageCategory.
|
||||
|
||||
|
||||
179
invokeai/app/services/videos_common.py
Normal file
179
invokeai/app/services/videos_common.py
Normal file
@@ -0,0 +1,179 @@
|
||||
import datetime
|
||||
from typing import Optional, Union
|
||||
|
||||
from pydantic import BaseModel, Field, StrictBool, StrictStr
|
||||
|
||||
from invokeai.app.util.misc import get_iso_timestamp
|
||||
from invokeai.app.util.model_exclude_null import BaseModelExcludeNull
|
||||
|
||||
VIDEO_DTO_COLS = ", ".join(
|
||||
[
|
||||
"videos." + c
|
||||
for c in [
|
||||
"video_id",
|
||||
"width",
|
||||
"height",
|
||||
"session_id",
|
||||
"node_id",
|
||||
"is_intermediate",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"deleted_at",
|
||||
"starred",
|
||||
]
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class VideoRecord(BaseModelExcludeNull):
|
||||
"""Deserialized video record without metadata."""
|
||||
|
||||
video_id: str = Field(description="The unique id of the video.")
|
||||
"""The unique id of the video."""
|
||||
width: int = Field(description="The width of the video in px.")
|
||||
"""The actual width of the video in px. This may be different from the width in metadata."""
|
||||
height: int = Field(description="The height of the video in px.")
|
||||
"""The actual height of the video in px. This may be different from the height in metadata."""
|
||||
created_at: Union[datetime.datetime, str] = Field(description="The created timestamp of the video.")
|
||||
"""The created timestamp of the video."""
|
||||
updated_at: Union[datetime.datetime, str] = Field(description="The updated timestamp of the video.")
|
||||
"""The updated timestamp of the video."""
|
||||
deleted_at: Optional[Union[datetime.datetime, str]] = Field(
|
||||
default=None, description="The deleted timestamp of the video."
|
||||
)
|
||||
"""The deleted timestamp of the video."""
|
||||
is_intermediate: bool = Field(description="Whether this is an intermediate video.")
|
||||
"""Whether this is an intermediate video."""
|
||||
session_id: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The session ID that generated this video, if it is a generated video.",
|
||||
)
|
||||
"""The session ID that generated this video, if it is a generated video."""
|
||||
node_id: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The node ID that generated this video, if it is a generated video.",
|
||||
)
|
||||
"""The node ID that generated this video, if it is a generated video."""
|
||||
starred: bool = Field(description="Whether this video is starred.")
|
||||
"""Whether this video is starred."""
|
||||
|
||||
|
||||
class VideoRecordChanges(BaseModelExcludeNull):
|
||||
"""A set of changes to apply to a video record.
|
||||
|
||||
Only limited changes are valid:
|
||||
- `session_id`: change the session associated with a video
|
||||
- `is_intermediate`: change the video's `is_intermediate` flag
|
||||
- `starred`: change whether the video is starred
|
||||
"""
|
||||
|
||||
session_id: Optional[StrictStr] = Field(
|
||||
default=None,
|
||||
description="The video's new session ID.",
|
||||
)
|
||||
"""The video's new session ID."""
|
||||
is_intermediate: Optional[StrictBool] = Field(default=None, description="The video's new `is_intermediate` flag.")
|
||||
"""The video's new `is_intermediate` flag."""
|
||||
starred: Optional[StrictBool] = Field(default=None, description="The video's new `starred` state")
|
||||
"""The video's new `starred` state."""
|
||||
|
||||
|
||||
def deserialize_video_record(video_dict: dict) -> VideoRecord:
|
||||
"""Deserializes a video record."""
|
||||
|
||||
# Retrieve all the values, setting "reasonable" defaults if they are not present.
|
||||
video_id = video_dict.get("video_id", "unknown")
|
||||
width = video_dict.get("width", 0)
|
||||
height = video_dict.get("height", 0)
|
||||
session_id = video_dict.get("session_id", None)
|
||||
node_id = video_dict.get("node_id", None)
|
||||
created_at = video_dict.get("created_at", get_iso_timestamp())
|
||||
updated_at = video_dict.get("updated_at", get_iso_timestamp())
|
||||
deleted_at = video_dict.get("deleted_at", get_iso_timestamp())
|
||||
is_intermediate = video_dict.get("is_intermediate", False)
|
||||
starred = video_dict.get("starred", False)
|
||||
|
||||
return VideoRecord(
|
||||
video_id=video_id,
|
||||
width=width,
|
||||
height=height,
|
||||
session_id=session_id,
|
||||
node_id=node_id,
|
||||
created_at=created_at,
|
||||
updated_at=updated_at,
|
||||
deleted_at=deleted_at,
|
||||
is_intermediate=is_intermediate,
|
||||
starred=starred,
|
||||
)
|
||||
|
||||
|
||||
class VideoCollectionCounts(BaseModel):
|
||||
starred_count: int = Field(description="The number of starred videos in the collection.")
|
||||
unstarred_count: int = Field(description="The number of unstarred videos in the collection.")
|
||||
|
||||
|
||||
class VideoIdsResult(BaseModel):
|
||||
"""Response containing ordered video ids with metadata for optimistic updates."""
|
||||
|
||||
video_ids: list[str] = Field(description="Ordered list of video ids")
|
||||
starred_count: int = Field(description="Number of starred videos (when starred_first=True)")
|
||||
total_count: int = Field(description="Total number of videos matching the query")
|
||||
|
||||
|
||||
class VideoUrlsDTO(BaseModelExcludeNull):
|
||||
"""The URLs for an image and its thumbnail."""
|
||||
|
||||
video_id: str = Field(description="The unique id of the video.")
|
||||
"""The unique id of the video."""
|
||||
video_url: str = Field(description="The URL of the video.")
|
||||
"""The URL of the video."""
|
||||
thumbnail_url: str = Field(description="The URL of the video's thumbnail.")
|
||||
"""The URL of the video's thumbnail."""
|
||||
|
||||
|
||||
class VideoDTO(VideoRecord, VideoUrlsDTO):
|
||||
"""Deserialized video record, enriched for the frontend."""
|
||||
|
||||
board_id: Optional[str] = Field(
|
||||
default=None, description="The id of the board the image belongs to, if one exists."
|
||||
)
|
||||
"""The id of the board the image belongs to, if one exists."""
|
||||
|
||||
|
||||
def video_record_to_dto(
|
||||
video_record: VideoRecord,
|
||||
video_url: str,
|
||||
thumbnail_url: str,
|
||||
board_id: Optional[str],
|
||||
) -> VideoDTO:
|
||||
"""Converts a video record to a video DTO."""
|
||||
return VideoDTO(
|
||||
**video_record.model_dump(),
|
||||
video_url=video_url,
|
||||
thumbnail_url=thumbnail_url,
|
||||
board_id=board_id,
|
||||
)
|
||||
|
||||
|
||||
class ResultWithAffectedBoards(BaseModel):
|
||||
affected_boards: list[str] = Field(description="The ids of boards affected by the delete operation")
|
||||
|
||||
|
||||
class DeleteVideosResult(ResultWithAffectedBoards):
|
||||
deleted_videos: list[str] = Field(description="The ids of the videos that were deleted")
|
||||
|
||||
|
||||
class StarredVideosResult(ResultWithAffectedBoards):
|
||||
starred_videos: list[str] = Field(description="The ids of the videos that were starred")
|
||||
|
||||
|
||||
class UnstarredVideosResult(ResultWithAffectedBoards):
|
||||
unstarred_videos: list[str] = Field(description="The ids of the videos that were unstarred")
|
||||
|
||||
|
||||
class AddVideosToBoardResult(ResultWithAffectedBoards):
|
||||
added_videos: list[str] = Field(description="The video ids that were added to the board")
|
||||
|
||||
|
||||
class RemoveVideosFromBoardResult(ResultWithAffectedBoards):
|
||||
removed_videos: list[str] = Field(description="The video ids that were removed from their board")
|
||||
@@ -492,6 +492,15 @@ class MainConfigBase(ABC, BaseModel):
|
||||
variant: AnyVariant = ModelVariantType.Normal
|
||||
|
||||
|
||||
class VideoConfigBase(ABC, BaseModel):
|
||||
type: Literal[ModelType.Video] = ModelType.Video
|
||||
trigger_phrases: Optional[set[str]] = Field(description="Set of trigger phrases for this model", default=None)
|
||||
default_settings: Optional[MainModelDefaultSettings] = Field(
|
||||
description="Default settings for this model", default=None
|
||||
)
|
||||
variant: AnyVariant = ModelVariantType.Normal
|
||||
|
||||
|
||||
class MainCheckpointConfig(CheckpointConfigBase, MainConfigBase, LegacyProbeMixin, ModelConfigBase):
|
||||
"""Model config for main checkpoint models."""
|
||||
|
||||
@@ -649,6 +658,21 @@ class ApiModelConfig(MainConfigBase, ModelConfigBase):
|
||||
raise NotImplementedError("API models are not parsed from disk.")
|
||||
|
||||
|
||||
class VideoApiModelConfig(VideoConfigBase, ModelConfigBase):
|
||||
"""Model config for API-based video models."""
|
||||
|
||||
format: Literal[ModelFormat.Api] = ModelFormat.Api
|
||||
|
||||
@classmethod
|
||||
def matches(cls, mod: ModelOnDisk) -> bool:
|
||||
# API models are not stored on disk, so we can't match them.
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def parse(cls, mod: ModelOnDisk) -> dict[str, Any]:
|
||||
raise NotImplementedError("API models are not parsed from disk.")
|
||||
|
||||
|
||||
def get_model_discriminator_value(v: Any) -> str:
|
||||
"""
|
||||
Computes the discriminator value for a model config.
|
||||
@@ -718,6 +742,7 @@ AnyModelConfig = Annotated[
|
||||
Annotated[FluxReduxConfig, FluxReduxConfig.get_tag()],
|
||||
Annotated[LlavaOnevisionConfig, LlavaOnevisionConfig.get_tag()],
|
||||
Annotated[ApiModelConfig, ApiModelConfig.get_tag()],
|
||||
Annotated[VideoApiModelConfig, VideoApiModelConfig.get_tag()],
|
||||
],
|
||||
Discriminator(get_model_discriminator_value),
|
||||
]
|
||||
|
||||
@@ -31,6 +31,8 @@ class BaseModelType(str, Enum):
|
||||
Gemini2_5 = "gemini-2.5"
|
||||
ChatGPT4o = "chatgpt-4o"
|
||||
FluxKontext = "flux-kontext"
|
||||
Veo3 = "veo3"
|
||||
Runway = "runway"
|
||||
|
||||
|
||||
class ModelType(str, Enum):
|
||||
@@ -52,6 +54,7 @@ class ModelType(str, Enum):
|
||||
SigLIP = "siglip"
|
||||
FluxRedux = "flux_redux"
|
||||
LlavaOnevision = "llava_onevision"
|
||||
Video = "video"
|
||||
|
||||
|
||||
class SubModelType(str, Enum):
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
"linkify-react": "^4.3.1",
|
||||
"linkifyjs": "^4.3.1",
|
||||
"lru-cache": "^11.1.0",
|
||||
"media-chrome": "^4.13.0",
|
||||
"mtwist": "^1.0.2",
|
||||
"nanoid": "^5.1.5",
|
||||
"nanostores": "^1.0.1",
|
||||
@@ -87,6 +88,7 @@
|
||||
"react-hotkeys-hook": "4.5.0",
|
||||
"react-i18next": "^15.5.3",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-player": "^3.3.1",
|
||||
"react-redux": "9.2.0",
|
||||
"react-resizable-panels": "^3.0.3",
|
||||
"react-textarea-autosize": "^8.5.9",
|
||||
|
||||
355
invokeai/frontend/web/pnpm-lock.yaml
generated
355
invokeai/frontend/web/pnpm-lock.yaml
generated
@@ -98,6 +98,9 @@ importers:
|
||||
lru-cache:
|
||||
specifier: ^11.1.0
|
||||
version: 11.1.0
|
||||
media-chrome:
|
||||
specifier: ^4.13.0
|
||||
version: 4.13.0(react@18.3.1)
|
||||
mtwist:
|
||||
specifier: ^1.0.2
|
||||
version: 1.0.2
|
||||
@@ -152,6 +155,9 @@ importers:
|
||||
react-icons:
|
||||
specifier: ^5.5.0
|
||||
version: 5.5.0(react@18.3.1)
|
||||
react-player:
|
||||
specifier: ^3.3.1
|
||||
version: 3.3.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react-redux:
|
||||
specifier: 9.2.0
|
||||
version: 9.2.0(@types/react@18.3.23)(react@18.3.1)(redux@5.0.1)
|
||||
@@ -937,6 +943,31 @@ packages:
|
||||
'@microsoft/tsdoc@0.15.1':
|
||||
resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==}
|
||||
|
||||
'@mux/mux-data-google-ima@0.2.8':
|
||||
resolution: {integrity: sha512-0ZEkHdcZ6bS8QtcjFcoJeZxJTpX7qRIledf4q1trMWPznugvtajCjCM2kieK/pzkZj1JM6liDRFs1PJSfVUs2A==}
|
||||
|
||||
'@mux/mux-player-react@3.5.3':
|
||||
resolution: {integrity: sha512-f0McZbIXYDkzecFwhhkf0JgEInPnsOClgBqBhkdhRlLRdrAzMATib+D3Di3rPkRHNH7rc/WWORvSxgJz6m6zkA==}
|
||||
peerDependencies:
|
||||
'@types/react': ^17.0.0 || ^17.0.0-0 || ^18 || ^18.0.0-0 || ^19 || ^19.0.0-0
|
||||
'@types/react-dom': '*'
|
||||
react: ^17.0.2 || ^17.0.0-0 || ^18 || ^18.0.0-0 || ^19 || ^19.0.0-0
|
||||
react-dom: ^17.0.2 || ^17.0.2-0 || ^18 || ^18.0.0-0 || ^19 || ^19.0.0-0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@mux/mux-player@3.5.3':
|
||||
resolution: {integrity: sha512-uXKFXbdtioAi+clSVfD60Rw4r7OvA62u2jV6aar9loW9qMsmKv8LU+8uaIaWQjyAORp6E0S37GOVjo72T6O2eQ==}
|
||||
|
||||
'@mux/mux-video@0.26.1':
|
||||
resolution: {integrity: sha512-gkMdBAgNlB4+krANZHkQFzYWjWeNsJz69y1/hnPtmNQnpvW+O7oc71OffcZrbblyibSxWMQ6MQpYmBVjXlp6sA==}
|
||||
|
||||
'@mux/playback-core@0.30.1':
|
||||
resolution: {integrity: sha512-rnO1NE9xHDyzbAkmE6ygJYcD7cyyMt7xXqWTykxlceaoSXLjUqgp42HDio7Lcidto4x/O4FIa7ztjV2aCBCXgQ==}
|
||||
|
||||
'@nanostores/react@0.7.3':
|
||||
resolution: {integrity: sha512-/XuLAMENRu/Q71biW4AZ4qmU070vkZgiQ28gaTSNRPm2SZF5zGAR81zPE1MaMB4SeOp6ZTst92NBaG75XSspNg==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
@@ -1453,6 +1484,9 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@svta/common-media-library@0.12.4':
|
||||
resolution: {integrity: sha512-9EuOoaNmz7JrfGwjsrD9SxF9otU5TNMnbLu1yU4BeLK0W5cDxVXXR58Z89q9u2AnHjIctscjMTYdlqQ1gojTuw==}
|
||||
|
||||
'@swc/core-darwin-arm64@1.12.9':
|
||||
resolution: {integrity: sha512-GACFEp4nD6V+TZNR2JwbMZRHB+Yyvp14FrcmB6UCUYmhuNWjkxi+CLnEvdbuiKyQYv0zA+TRpCHZ+whEs6gwfA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -1707,6 +1741,12 @@ packages:
|
||||
resolution: {integrity: sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@vercel/edge@1.2.2':
|
||||
resolution: {integrity: sha512-1+y+f6rk0Yc9ss9bRDgz/gdpLimwoRteKHhrcgHvEpjbP1nyT3ByqEMWm2BTcpIO5UtDmIFXc8zdq4LR190PDA==}
|
||||
|
||||
'@vimeo/player@2.29.0':
|
||||
resolution: {integrity: sha512-9JjvjeqUndb9otCCFd0/+2ESsLk7VkDE6sxOBy9iy2ukezuQbplVRi+g9g59yAurKofbmTi/KcKxBGO/22zWRw==}
|
||||
|
||||
'@vitejs/plugin-react-swc@3.10.2':
|
||||
resolution: {integrity: sha512-xD3Rdvrt5LgANug7WekBn1KhcvLn1H3jNBfJRL3reeOIua/WnZOEV5qi5qIBq5T8R0jUDmRtxuvk4bPhzGHDWw==}
|
||||
peerDependencies:
|
||||
@@ -1959,6 +1999,15 @@ packages:
|
||||
base64-js@1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
|
||||
bcp-47-match@2.0.3:
|
||||
resolution: {integrity: sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==}
|
||||
|
||||
bcp-47-normalize@2.3.0:
|
||||
resolution: {integrity: sha512-8I/wfzqQvttUFz7HVJgIZ7+dj3vUaIyIxYXaTRP1YWoSDfzt6TUmxaKZeuXR62qBmYr+nvuWINFRl6pZ5DlN4Q==}
|
||||
|
||||
bcp-47@2.1.0:
|
||||
resolution: {integrity: sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==}
|
||||
|
||||
better-opn@3.0.2:
|
||||
resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@@ -2014,6 +2063,14 @@ packages:
|
||||
caniuse-lite@1.0.30001727:
|
||||
resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==}
|
||||
|
||||
castable-video@1.1.10:
|
||||
resolution: {integrity: sha512-/T1I0A4VG769wTEZ8gWuy1Crn9saAfRTd1UYTb8xbOPlN78+zOi/1nU2dD5koNkfE5VWvgabkIqrGKmyNXOjSQ==}
|
||||
|
||||
ce-la-react@0.3.1:
|
||||
resolution: {integrity: sha512-g0YwpZDPIwTwFumGTzNHcgJA6VhFfFCJkSNdUdC04br2UfU+56JDrJrJva3FZ7MToB4NDHAFBiPE/PZdNl1mQA==}
|
||||
peerDependencies:
|
||||
react: '>=17.0.0'
|
||||
|
||||
chai@5.2.0:
|
||||
resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -2064,12 +2121,18 @@ packages:
|
||||
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
|
||||
engines: {node: '>=0.8'}
|
||||
|
||||
cloudflare-video-element@1.3.3:
|
||||
resolution: {integrity: sha512-qrHzwLmUhisoIuEoKc7iBbdzBNj2Pi7ThHslU/9U/6PY9DEvo4mh/U+w7OVuzXT9ks7ZXfARvDBfPAaMGF/hIg==}
|
||||
|
||||
cmdk@1.1.1:
|
||||
resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==}
|
||||
peerDependencies:
|
||||
react: ^18 || ^19 || ^19.0.0-rc
|
||||
react-dom: ^18 || ^19 || ^19.0.0-rc
|
||||
|
||||
codem-isoboxer@0.3.10:
|
||||
resolution: {integrity: sha512-eNk3TRV+xQMJ1PEj0FQGY8KD4m0GPxT487XJ+Iftm7mVa9WpPFDMWqPt+46buiP5j5Wzqe5oMIhqBcAeKfygSA==}
|
||||
|
||||
color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
@@ -2139,6 +2202,9 @@ packages:
|
||||
csstype@3.1.3:
|
||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||
|
||||
custom-media-element@1.4.5:
|
||||
resolution: {integrity: sha512-cjrsQufETwxjvwZbYbKBCJNvmQ2++G9AvT45zDi7NXL9k2PdVcs2h0jQz96J6G4TMKRCcEsoJ+QTgQD00Igtjw==}
|
||||
|
||||
d3-color@3.1.0:
|
||||
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -2177,6 +2243,12 @@ packages:
|
||||
resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
dash-video-element@0.1.6:
|
||||
resolution: {integrity: sha512-4gHShaQjcFv6diX5EzB6qAdUGKlIUGGZY8J8yp2pQkWqR0jX4c6plYy0cFraN7mr0DZINe8ujDN1fssDYxJjcg==}
|
||||
|
||||
dashjs@5.0.3:
|
||||
resolution: {integrity: sha512-TXndNnCUjFjF2nYBxDVba+hWRpVkadkQ8flLp7kHkem+5+wZTfRShJCnVkPUosmjS0YPE9fVNLbYPJxHBeQZvA==}
|
||||
|
||||
data-view-buffer@1.0.2:
|
||||
resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2751,9 +2823,18 @@ packages:
|
||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||
hasBin: true
|
||||
|
||||
hls-video-element@1.5.6:
|
||||
resolution: {integrity: sha512-KPdvSR+oBJPiCVb+m6pd2mn3rJEjNbaK8pGhSkxFI2pmyvZIeTVQrPbEO9PT/juwXHwhvCoKJnNxAuFwJG9H5A==}
|
||||
|
||||
hls.js@1.6.9:
|
||||
resolution: {integrity: sha512-q7qPrri6GRwjcNd7EkFCmhiJ6PBIxeUsdxKbquBkQZpg9jAnp6zSAeN9eEWFlOB09J8JfzAQGoXL5ZEAltjO9g==}
|
||||
|
||||
hoist-non-react-statics@3.3.2:
|
||||
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
|
||||
|
||||
html-entities@2.6.0:
|
||||
resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==}
|
||||
|
||||
html-escaper@2.0.2:
|
||||
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
||||
|
||||
@@ -2792,6 +2873,9 @@ packages:
|
||||
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
immediate@3.0.6:
|
||||
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
|
||||
|
||||
immer@10.1.1:
|
||||
resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==}
|
||||
|
||||
@@ -2803,6 +2887,9 @@ packages:
|
||||
resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
imsc@1.1.5:
|
||||
resolution: {integrity: sha512-V8je+CGkcvGhgl2C1GlhqFFiUOIEdwXbXLiu1Fcubvvbo+g9inauqT3l0pNYXGoLPBj3jxtZz9t+wCopMkwadQ==}
|
||||
|
||||
imurmurhash@0.1.4:
|
||||
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
|
||||
engines: {node: '>=0.8.19'}
|
||||
@@ -2825,6 +2912,12 @@ packages:
|
||||
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
is-alphabetical@2.0.1:
|
||||
resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
|
||||
|
||||
is-alphanumerical@2.0.1:
|
||||
resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==}
|
||||
|
||||
is-array-buffer@3.0.5:
|
||||
resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2860,6 +2953,9 @@ packages:
|
||||
resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
is-decimal@2.0.1:
|
||||
resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
|
||||
|
||||
is-docker@2.2.1:
|
||||
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -3064,6 +3160,9 @@ packages:
|
||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
lie@3.1.1:
|
||||
resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==}
|
||||
|
||||
lines-and-columns@1.2.4:
|
||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||
|
||||
@@ -3088,6 +3187,9 @@ packages:
|
||||
resolution: {integrity: sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
localforage@1.10.0:
|
||||
resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==}
|
||||
|
||||
locate-path@6.0.0:
|
||||
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -3154,6 +3256,15 @@ packages:
|
||||
mdn-data@2.0.14:
|
||||
resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==}
|
||||
|
||||
media-chrome@4.11.1:
|
||||
resolution: {integrity: sha512-+2niDc4qOwlpFAjwxg1OaizK/zKV6y7QqGm4nBFEVlSaG0ZBgOmfc4IXAPiirZqAlZGaFFUaMqCl1SpGU0/naA==}
|
||||
|
||||
media-chrome@4.13.0:
|
||||
resolution: {integrity: sha512-DfX/Hwxjae/tEHjr1tVnV/6XDFHriMXI1ev8Ji4Z/YwXnqMhNfRtvNsMjefnQK5pkMS/9hC+jmdS+VDWZfsSIw==}
|
||||
|
||||
media-tracks@0.3.3:
|
||||
resolution: {integrity: sha512-9P2FuUHnZZ3iji+2RQk7Zkh5AmZTnOG5fODACnjhCVveX1McY3jmCRHofIEI+yTBqplz7LXy48c7fQ3Uigp88w==}
|
||||
|
||||
memoize-one@6.0.0:
|
||||
resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
|
||||
|
||||
@@ -3219,6 +3330,12 @@ packages:
|
||||
muggle-string@0.4.1:
|
||||
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
|
||||
|
||||
mux-embed@5.11.0:
|
||||
resolution: {integrity: sha512-uczzXVraqMRmyYmpGh2zthTmBKvvc5D5yaVKQRgGhFOnF7E4nkhqNkdkQc4C0WTPzdqdPl5OtCelNWMF4tg5RQ==}
|
||||
|
||||
mux-embed@5.9.0:
|
||||
resolution: {integrity: sha512-wmunL3uoPhma/tWy8PrDPZkvJpXvSFBwbD3KkC4PG8Ztjfb1X3hRJwGUAQyRz7z99b/ovLm2UTTitrkvStjH4w==}
|
||||
|
||||
nano-css@5.6.2:
|
||||
resolution: {integrity: sha512-+6bHaC8dSDGALM1HJjOHVXpuastdu2xFoZlC77Jh4cg+33Zcgm+Gxd+1xsnpZK14eyHObSp82+ll5y3SX75liw==}
|
||||
peerDependencies:
|
||||
@@ -3243,6 +3360,9 @@ packages:
|
||||
resolution: {integrity: sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw==}
|
||||
engines: {node: ^20.0.0 || >=22.0.0}
|
||||
|
||||
native-promise-only@0.8.1:
|
||||
resolution: {integrity: sha512-zkVhZUA3y8mbz652WrL5x0fB0ehrBkulWT3TomAQ9iDtyXZvzKeEA6GPxAItBYeNYl5yngKRX612qHOhvMkDeg==}
|
||||
|
||||
natural-compare@1.4.0:
|
||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||
|
||||
@@ -3433,6 +3553,9 @@ packages:
|
||||
pkg-types@2.2.0:
|
||||
resolution: {integrity: sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==}
|
||||
|
||||
player.style@0.1.9:
|
||||
resolution: {integrity: sha512-aFmIhHMrnAP8YliFYFMnRw+5AlHqBvnqWy4vHGo2kFxlC+XjmTXqgg62qSxlE8ubAY83c0ViEZGYglSJi6mGCA==}
|
||||
|
||||
pluralize@8.0.0:
|
||||
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -3575,6 +3698,13 @@ packages:
|
||||
react-is@17.0.2:
|
||||
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
|
||||
|
||||
react-player@3.3.1:
|
||||
resolution: {integrity: sha512-wE/xLloneXZ1keelFCaNeIFVNUp4/7YoUjfHjwF945aQzsbDKiIB0LQuCchGL+la0Y1IybxnR0R6Cm3AiqInMw==}
|
||||
peerDependencies:
|
||||
'@types/react': ^17.0.0 || ^18 || ^19
|
||||
react: ^17.0.2 || ^18 || ^19
|
||||
react-dom: ^17.0.2 || ^18 || ^19
|
||||
|
||||
react-redux@9.2.0:
|
||||
resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==}
|
||||
peerDependencies:
|
||||
@@ -3809,6 +3939,9 @@ packages:
|
||||
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
sax@1.2.1:
|
||||
resolution: {integrity: sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==}
|
||||
|
||||
scheduler@0.23.2:
|
||||
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
|
||||
|
||||
@@ -3931,6 +4064,9 @@ packages:
|
||||
resolution: {integrity: sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
spotify-audio-element@1.0.2:
|
||||
resolution: {integrity: sha512-YEovyyeJTJMzdSVqFw/Fx19e1gdcD4bmZZ/fWS0Ji58KTpvAT2rophgK87ocqpy6eJNSmIHikhgbRjGWumgZew==}
|
||||
|
||||
sprintf-js@1.0.3:
|
||||
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
|
||||
|
||||
@@ -4039,6 +4175,9 @@ packages:
|
||||
stylis@4.3.6:
|
||||
resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==}
|
||||
|
||||
super-media-element@1.4.2:
|
||||
resolution: {integrity: sha512-9pP/CVNp4NF2MNlRzLwQkjiTgKKe9WYXrLh9+8QokWmMxz+zt2mf1utkWLco26IuA3AfVcTb//qtlTIjY3VHxA==}
|
||||
|
||||
supports-color@10.0.0:
|
||||
resolution: {integrity: sha512-HRVVSbCCMbj7/kdWF9Q+bbckjBHLtHMEoJWlkmYzzdwhYMkjkOwubLM6t7NbWKjgKamGDrWL1++KrjUO1t9oAQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -4063,6 +4202,9 @@ packages:
|
||||
resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
tiktok-video-element@0.1.0:
|
||||
resolution: {integrity: sha512-PVWUlpDdQ/LPXi7x4/furfD7Xh1L72CgkGCaMsZBIjvxucMGm1DDPJdM9IhWBFfo6tuR4cYVO/v596r6GG/lvQ==}
|
||||
|
||||
tiny-invariant@1.3.3:
|
||||
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||
|
||||
@@ -4148,6 +4290,9 @@ packages:
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
twitch-video-element@0.1.2:
|
||||
resolution: {integrity: sha512-/up4KiWiTYiav+CUo+/DbV8JhP4COwEKSo8h1H/Zft/5NzZ/ZiIQ48h7erFKvwzalN0GfkEGGIfwIzAO0h7FHQ==}
|
||||
|
||||
type-check@0.4.0:
|
||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -4182,6 +4327,10 @@ packages:
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
ua-parser-js@1.0.40:
|
||||
resolution: {integrity: sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==}
|
||||
hasBin: true
|
||||
|
||||
ufo@1.6.1:
|
||||
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
|
||||
|
||||
@@ -4286,6 +4435,9 @@ packages:
|
||||
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
|
||||
hasBin: true
|
||||
|
||||
vimeo-video-element@1.5.3:
|
||||
resolution: {integrity: sha512-OQWyGS9nTouMqfRJyvmAm/n6IRhZ7x3EfPAef+Q+inGBeHa3SylDbtyeB/rEBd4B/T/lcYBW3rjaD9W2DRYkiQ==}
|
||||
|
||||
vite-node@3.2.4:
|
||||
resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
|
||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||
@@ -4401,6 +4553,10 @@ packages:
|
||||
wcwidth@1.0.1:
|
||||
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
|
||||
|
||||
weakmap-polyfill@2.0.4:
|
||||
resolution: {integrity: sha512-ZzxBf288iALJseijWelmECm/1x7ZwQn3sMYIkDr2VvZp7r6SEKuT8D0O9Wiq6L9Nl5mazrOMcmiZE/2NCenaxw==}
|
||||
engines: {node: '>=8.10.0'}
|
||||
|
||||
webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
|
||||
@@ -4436,6 +4592,9 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
hasBin: true
|
||||
|
||||
wistia-video-element@1.3.3:
|
||||
resolution: {integrity: sha512-ZVC8HH8uV3mQGcSz10MACLDalao/0YdVverNN4GNFsOXiumfqSiZnRVc8WZEywgVckBkR7+yerQYESYPDzvTfQ==}
|
||||
|
||||
word-wrap@1.2.5:
|
||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -4509,6 +4668,9 @@ packages:
|
||||
resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==}
|
||||
engines: {node: '>=12.20'}
|
||||
|
||||
youtube-video-element@1.6.1:
|
||||
resolution: {integrity: sha512-FDRgXlPxpe1bh6HlhL6GfJVcvVNaZKCcLEZ90X1G3Iu+z2g2cIhm2OWj9abPZq1Zqit6SY7Gwh13H9g7acoBnQ==}
|
||||
|
||||
zod-validation-error@3.5.3:
|
||||
resolution: {integrity: sha512-OT5Y8lbUadqVZCsnyFaTQ4/O2mys4tj7PqhdbBCp7McPwvIEKfPtdA6QfPeFQK2/Rz5LgwmAXRJTugBNBi0btw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
@@ -5235,6 +5397,43 @@ snapshots:
|
||||
|
||||
'@microsoft/tsdoc@0.15.1': {}
|
||||
|
||||
'@mux/mux-data-google-ima@0.2.8':
|
||||
dependencies:
|
||||
mux-embed: 5.9.0
|
||||
|
||||
'@mux/mux-player-react@3.5.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@mux/mux-player': 3.5.3(react@18.3.1)
|
||||
'@mux/playback-core': 0.30.1
|
||||
prop-types: 15.8.1
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.23
|
||||
'@types/react-dom': 18.3.7(@types/react@18.3.23)
|
||||
|
||||
'@mux/mux-player@3.5.3(react@18.3.1)':
|
||||
dependencies:
|
||||
'@mux/mux-video': 0.26.1
|
||||
'@mux/playback-core': 0.30.1
|
||||
media-chrome: 4.11.1(react@18.3.1)
|
||||
player.style: 0.1.9(react@18.3.1)
|
||||
transitivePeerDependencies:
|
||||
- react
|
||||
|
||||
'@mux/mux-video@0.26.1':
|
||||
dependencies:
|
||||
'@mux/mux-data-google-ima': 0.2.8
|
||||
'@mux/playback-core': 0.30.1
|
||||
castable-video: 1.1.10
|
||||
custom-media-element: 1.4.5
|
||||
media-tracks: 0.3.3
|
||||
|
||||
'@mux/playback-core@0.30.1':
|
||||
dependencies:
|
||||
hls.js: 1.6.9
|
||||
mux-embed: 5.11.0
|
||||
|
||||
'@nanostores/react@0.7.3(nanostores@0.11.4)(react@18.3.1)':
|
||||
dependencies:
|
||||
nanostores: 0.11.4
|
||||
@@ -5690,6 +5889,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
typescript: 5.8.3
|
||||
|
||||
'@svta/common-media-library@0.12.4': {}
|
||||
|
||||
'@swc/core-darwin-arm64@1.12.9':
|
||||
optional: true
|
||||
|
||||
@@ -5971,6 +6172,13 @@ snapshots:
|
||||
'@typescript-eslint/types': 8.37.0
|
||||
eslint-visitor-keys: 4.2.1
|
||||
|
||||
'@vercel/edge@1.2.2': {}
|
||||
|
||||
'@vimeo/player@2.29.0':
|
||||
dependencies:
|
||||
native-promise-only: 0.8.1
|
||||
weakmap-polyfill: 2.0.4
|
||||
|
||||
'@vitejs/plugin-react-swc@3.10.2(vite@7.0.5(@types/node@22.16.0)(jiti@2.4.2))':
|
||||
dependencies:
|
||||
'@rolldown/pluginutils': 1.0.0-beta.11
|
||||
@@ -6304,6 +6512,19 @@ snapshots:
|
||||
|
||||
base64-js@1.5.1: {}
|
||||
|
||||
bcp-47-match@2.0.3: {}
|
||||
|
||||
bcp-47-normalize@2.3.0:
|
||||
dependencies:
|
||||
bcp-47: 2.1.0
|
||||
bcp-47-match: 2.0.3
|
||||
|
||||
bcp-47@2.1.0:
|
||||
dependencies:
|
||||
is-alphabetical: 2.0.1
|
||||
is-alphanumerical: 2.0.1
|
||||
is-decimal: 2.0.1
|
||||
|
||||
better-opn@3.0.2:
|
||||
dependencies:
|
||||
open: 8.4.2
|
||||
@@ -6366,6 +6587,14 @@ snapshots:
|
||||
|
||||
caniuse-lite@1.0.30001727: {}
|
||||
|
||||
castable-video@1.1.10:
|
||||
dependencies:
|
||||
custom-media-element: 1.4.5
|
||||
|
||||
ce-la-react@0.3.1(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
|
||||
chai@5.2.0:
|
||||
dependencies:
|
||||
assertion-error: 2.0.1
|
||||
@@ -6423,6 +6652,8 @@ snapshots:
|
||||
|
||||
clone@1.0.4: {}
|
||||
|
||||
cloudflare-video-element@1.3.3: {}
|
||||
|
||||
cmdk@1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1)
|
||||
@@ -6435,6 +6666,8 @@ snapshots:
|
||||
- '@types/react'
|
||||
- '@types/react-dom'
|
||||
|
||||
codem-isoboxer@0.3.10: {}
|
||||
|
||||
color-convert@2.0.1:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
@@ -6510,6 +6743,8 @@ snapshots:
|
||||
|
||||
csstype@3.1.3: {}
|
||||
|
||||
custom-media-element@1.4.5: {}
|
||||
|
||||
d3-color@3.1.0: {}
|
||||
|
||||
d3-dispatch@3.0.1: {}
|
||||
@@ -6546,6 +6781,24 @@ snapshots:
|
||||
d3-selection: 3.0.0
|
||||
d3-transition: 3.0.1(d3-selection@3.0.0)
|
||||
|
||||
dash-video-element@0.1.6:
|
||||
dependencies:
|
||||
custom-media-element: 1.4.5
|
||||
dashjs: 5.0.3
|
||||
|
||||
dashjs@5.0.3:
|
||||
dependencies:
|
||||
'@svta/common-media-library': 0.12.4
|
||||
bcp-47-match: 2.0.3
|
||||
bcp-47-normalize: 2.3.0
|
||||
codem-isoboxer: 0.3.10
|
||||
fast-deep-equal: 3.1.3
|
||||
html-entities: 2.6.0
|
||||
imsc: 1.1.5
|
||||
localforage: 1.10.0
|
||||
path-browserify: 1.0.1
|
||||
ua-parser-js: 1.0.40
|
||||
|
||||
data-view-buffer@1.0.2:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
@@ -7244,10 +7497,20 @@ snapshots:
|
||||
|
||||
he@1.2.0: {}
|
||||
|
||||
hls-video-element@1.5.6:
|
||||
dependencies:
|
||||
custom-media-element: 1.4.5
|
||||
hls.js: 1.6.9
|
||||
media-tracks: 0.3.3
|
||||
|
||||
hls.js@1.6.9: {}
|
||||
|
||||
hoist-non-react-statics@3.3.2:
|
||||
dependencies:
|
||||
react-is: 16.13.1
|
||||
|
||||
html-entities@2.6.0: {}
|
||||
|
||||
html-escaper@2.0.2: {}
|
||||
|
||||
html-parse-stringify@3.0.1:
|
||||
@@ -7283,6 +7546,8 @@ snapshots:
|
||||
|
||||
ignore@7.0.5: {}
|
||||
|
||||
immediate@3.0.6: {}
|
||||
|
||||
immer@10.1.1: {}
|
||||
|
||||
import-fresh@3.3.1:
|
||||
@@ -7292,6 +7557,10 @@ snapshots:
|
||||
|
||||
import-lazy@4.0.0: {}
|
||||
|
||||
imsc@1.1.5:
|
||||
dependencies:
|
||||
sax: 1.2.1
|
||||
|
||||
imurmurhash@0.1.4: {}
|
||||
|
||||
indent-string@4.0.0: {}
|
||||
@@ -7310,6 +7579,13 @@ snapshots:
|
||||
hasown: 2.0.2
|
||||
side-channel: 1.1.0
|
||||
|
||||
is-alphabetical@2.0.1: {}
|
||||
|
||||
is-alphanumerical@2.0.1:
|
||||
dependencies:
|
||||
is-alphabetical: 2.0.1
|
||||
is-decimal: 2.0.1
|
||||
|
||||
is-array-buffer@3.0.5:
|
||||
dependencies:
|
||||
call-bind: 1.0.8
|
||||
@@ -7352,6 +7628,8 @@ snapshots:
|
||||
call-bound: 1.0.4
|
||||
has-tostringtag: 1.0.2
|
||||
|
||||
is-decimal@2.0.1: {}
|
||||
|
||||
is-docker@2.2.1: {}
|
||||
|
||||
is-extglob@2.1.1: {}
|
||||
@@ -7553,6 +7831,10 @@ snapshots:
|
||||
prelude-ls: 1.2.1
|
||||
type-check: 0.4.0
|
||||
|
||||
lie@3.1.1:
|
||||
dependencies:
|
||||
immediate: 3.0.6
|
||||
|
||||
lines-and-columns@1.2.4: {}
|
||||
|
||||
linkify-react@4.3.1(linkifyjs@4.3.1)(react@18.3.1):
|
||||
@@ -7575,6 +7857,10 @@ snapshots:
|
||||
pkg-types: 2.2.0
|
||||
quansync: 0.2.10
|
||||
|
||||
localforage@1.10.0:
|
||||
dependencies:
|
||||
lie: 3.1.1
|
||||
|
||||
locate-path@6.0.0:
|
||||
dependencies:
|
||||
p-locate: 5.0.0
|
||||
@@ -7634,6 +7920,21 @@ snapshots:
|
||||
|
||||
mdn-data@2.0.14: {}
|
||||
|
||||
media-chrome@4.11.1(react@18.3.1):
|
||||
dependencies:
|
||||
'@vercel/edge': 1.2.2
|
||||
ce-la-react: 0.3.1(react@18.3.1)
|
||||
transitivePeerDependencies:
|
||||
- react
|
||||
|
||||
media-chrome@4.13.0(react@18.3.1):
|
||||
dependencies:
|
||||
ce-la-react: 0.3.1(react@18.3.1)
|
||||
transitivePeerDependencies:
|
||||
- react
|
||||
|
||||
media-tracks@0.3.3: {}
|
||||
|
||||
memoize-one@6.0.0: {}
|
||||
|
||||
merge2@1.4.1: {}
|
||||
@@ -7690,6 +7991,10 @@ snapshots:
|
||||
|
||||
muggle-string@0.4.1: {}
|
||||
|
||||
mux-embed@5.11.0: {}
|
||||
|
||||
mux-embed@5.9.0: {}
|
||||
|
||||
nano-css@5.6.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.4
|
||||
@@ -7711,6 +8016,8 @@ snapshots:
|
||||
|
||||
nanostores@1.0.1: {}
|
||||
|
||||
native-promise-only@0.8.1: {}
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
|
||||
nearley@2.20.1:
|
||||
@@ -7929,6 +8236,12 @@ snapshots:
|
||||
exsolve: 1.0.7
|
||||
pathe: 2.0.3
|
||||
|
||||
player.style@0.1.9(react@18.3.1):
|
||||
dependencies:
|
||||
media-chrome: 4.11.1(react@18.3.1)
|
||||
transitivePeerDependencies:
|
||||
- react
|
||||
|
||||
pluralize@8.0.0: {}
|
||||
|
||||
possible-typed-array-names@1.1.0: {}
|
||||
@@ -8066,6 +8379,24 @@ snapshots:
|
||||
|
||||
react-is@17.0.2: {}
|
||||
|
||||
react-player@3.3.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@mux/mux-player-react': 3.5.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@types/react': 18.3.23
|
||||
cloudflare-video-element: 1.3.3
|
||||
dash-video-element: 0.1.6
|
||||
hls-video-element: 1.5.6
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
spotify-audio-element: 1.0.2
|
||||
tiktok-video-element: 0.1.0
|
||||
twitch-video-element: 0.1.2
|
||||
vimeo-video-element: 1.5.3
|
||||
wistia-video-element: 1.3.3
|
||||
youtube-video-element: 1.6.1
|
||||
transitivePeerDependencies:
|
||||
- '@types/react-dom'
|
||||
|
||||
react-redux@9.2.0(@types/react@18.3.23)(react@18.3.1)(redux@5.0.1):
|
||||
dependencies:
|
||||
'@types/use-sync-external-store': 0.0.6
|
||||
@@ -8360,6 +8691,8 @@ snapshots:
|
||||
|
||||
safe-stable-stringify@2.5.0: {}
|
||||
|
||||
sax@1.2.1: {}
|
||||
|
||||
scheduler@0.23.2:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
@@ -8484,6 +8817,8 @@ snapshots:
|
||||
|
||||
split-on-first@3.0.0: {}
|
||||
|
||||
spotify-audio-element@1.0.2: {}
|
||||
|
||||
sprintf-js@1.0.3: {}
|
||||
|
||||
stable-hash@0.0.6: {}
|
||||
@@ -8627,6 +8962,8 @@ snapshots:
|
||||
|
||||
stylis@4.3.6: {}
|
||||
|
||||
super-media-element@1.4.2: {}
|
||||
|
||||
supports-color@10.0.0: {}
|
||||
|
||||
supports-color@7.2.0:
|
||||
@@ -8647,6 +8984,8 @@ snapshots:
|
||||
|
||||
throttle-debounce@3.0.1: {}
|
||||
|
||||
tiktok-video-element@0.1.0: {}
|
||||
|
||||
tiny-invariant@1.3.3: {}
|
||||
|
||||
tinybench@2.9.0: {}
|
||||
@@ -8709,6 +9048,8 @@ snapshots:
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
twitch-video-element@0.1.2: {}
|
||||
|
||||
type-check@0.4.0:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
@@ -8752,6 +9093,8 @@ snapshots:
|
||||
|
||||
typescript@5.8.3: {}
|
||||
|
||||
ua-parser-js@1.0.40: {}
|
||||
|
||||
ufo@1.6.1: {}
|
||||
|
||||
unbox-primitive@1.1.0:
|
||||
@@ -8834,6 +9177,10 @@ snapshots:
|
||||
|
||||
uuid@11.1.0: {}
|
||||
|
||||
vimeo-video-element@1.5.3:
|
||||
dependencies:
|
||||
'@vimeo/player': 2.29.0
|
||||
|
||||
vite-node@3.2.4(@types/node@22.16.0)(jiti@2.4.2):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
@@ -8962,6 +9309,8 @@ snapshots:
|
||||
dependencies:
|
||||
defaults: 1.0.4
|
||||
|
||||
weakmap-polyfill@2.0.4: {}
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
||||
webpack-virtual-modules@0.6.2: {}
|
||||
@@ -9021,6 +9370,10 @@ snapshots:
|
||||
siginfo: 2.0.0
|
||||
stackback: 0.0.2
|
||||
|
||||
wistia-video-element@1.3.3:
|
||||
dependencies:
|
||||
super-media-element: 1.4.2
|
||||
|
||||
word-wrap@1.2.5: {}
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
@@ -9067,6 +9420,8 @@ snapshots:
|
||||
|
||||
yocto-queue@1.2.1: {}
|
||||
|
||||
youtube-video-element@1.6.1: {}
|
||||
|
||||
zod-validation-error@3.5.3(zod@3.25.76):
|
||||
dependencies:
|
||||
zod: 3.25.76
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
"move": "Move",
|
||||
"movingImagesToBoard_one": "Moving {{count}} image to board:",
|
||||
"movingImagesToBoard_other": "Moving {{count}} images to board:",
|
||||
"movingVideosToBoard_one": "Moving {{count}} video to board:",
|
||||
"movingVideosToBoard_other": "Moving {{count}} videos to board:",
|
||||
"myBoard": "My Board",
|
||||
"noBoards": "No {{boardType}} Boards",
|
||||
"noMatching": "No matching Boards",
|
||||
@@ -59,6 +61,8 @@
|
||||
"imagesWithCount_other": "{{count}} images",
|
||||
"assetsWithCount_one": "{{count}} asset",
|
||||
"assetsWithCount_other": "{{count}} assets",
|
||||
"videosWithCount_one": "{{count}} video",
|
||||
"videosWithCount_other": "{{count}} videos",
|
||||
"updateBoardError": "Error updating board"
|
||||
},
|
||||
"accordions": {
|
||||
@@ -361,6 +365,9 @@
|
||||
"deleteImage_one": "Delete Image",
|
||||
"deleteImage_other": "Delete {{count}} Images",
|
||||
"deleteImagePermanent": "Deleted images cannot be restored.",
|
||||
"deleteVideo_one": "Delete Video",
|
||||
"deleteVideo_other": "Delete {{count}} Videos",
|
||||
"deleteVideoPermanent": "Deleted videos cannot be restored.",
|
||||
"displayBoardSearch": "Board Search",
|
||||
"displaySearch": "Image Search",
|
||||
"download": "Download",
|
||||
@@ -380,9 +387,10 @@
|
||||
"sortDirection": "Sort Direction",
|
||||
"showStarredImagesFirst": "Show Starred Images First",
|
||||
"noImageSelected": "No Image Selected",
|
||||
"noVideoSelected": "No Video Selected",
|
||||
"noImagesInGallery": "No Images to Display",
|
||||
"starImage": "Star Image",
|
||||
"unstarImage": "Unstar Image",
|
||||
"starImage": "Star",
|
||||
"unstarImage": "Unstar",
|
||||
"unableToLoad": "Unable to load Gallery",
|
||||
"deleteSelection": "Delete Selection",
|
||||
"downloadSelection": "Download Selection",
|
||||
@@ -411,7 +419,9 @@
|
||||
"openViewer": "Open Viewer",
|
||||
"closeViewer": "Close Viewer",
|
||||
"move": "Move",
|
||||
"useForPromptGeneration": "Use for Prompt Generation"
|
||||
"useForPromptGeneration": "Use for Prompt Generation",
|
||||
"videos": "Videos",
|
||||
"videosTab": "Videos you've created and saved within Invoke."
|
||||
},
|
||||
"hotkeys": {
|
||||
"hotkeys": "Hotkeys",
|
||||
@@ -456,6 +466,10 @@
|
||||
"title": "Select the Queue Tab",
|
||||
"desc": "Selects the Queue tab."
|
||||
},
|
||||
"selectVideoTab": {
|
||||
"title": "Select the Video Tab",
|
||||
"desc": "Selects the Video tab."
|
||||
},
|
||||
"focusPrompt": {
|
||||
"title": "Focus Prompt",
|
||||
"desc": "Move cursor focus to the positive prompt."
|
||||
@@ -482,6 +496,9 @@
|
||||
"key": "1"
|
||||
}
|
||||
},
|
||||
"video": {
|
||||
"title": "Video"
|
||||
},
|
||||
"canvas": {
|
||||
"title": "Canvas",
|
||||
"selectBrushTool": {
|
||||
@@ -781,11 +798,13 @@
|
||||
"guidance": "Guidance",
|
||||
"height": "Height",
|
||||
"imageDetails": "Image Details",
|
||||
"videoDetails": "Video Details",
|
||||
"imageDimensions": "Image Dimensions",
|
||||
"metadata": "Metadata",
|
||||
"model": "Model",
|
||||
"negativePrompt": "Negative Prompt",
|
||||
"noImageDetails": "No image details found",
|
||||
"noVideoDetails": "No video details found",
|
||||
"noMetaData": "No metadata found",
|
||||
"noRecallParameters": "No parameters to recall found",
|
||||
"parameterSet": "Parameter {{parameter}} set",
|
||||
@@ -803,7 +822,11 @@
|
||||
"vae": "VAE",
|
||||
"width": "Width",
|
||||
"workflow": "Workflow",
|
||||
"canvasV2Metadata": "Canvas Layers"
|
||||
"canvasV2Metadata": "Canvas Layers",
|
||||
"videoModel": "Model",
|
||||
"videoDuration": "Duration",
|
||||
"videoAspectRatio": "Aspect Ratio",
|
||||
"videoResolution": "Resolution"
|
||||
},
|
||||
"modelManager": {
|
||||
"active": "active",
|
||||
@@ -1189,6 +1212,7 @@
|
||||
},
|
||||
"parameters": {
|
||||
"aspect": "Aspect",
|
||||
"duration": "Duration",
|
||||
"lockAspectRatio": "Lock Aspect Ratio",
|
||||
"swapDimensions": "Swap Dimensions",
|
||||
"setToOptimalSize": "Optimize size for model",
|
||||
@@ -1213,9 +1237,14 @@
|
||||
"height": "Height",
|
||||
"imageFit": "Fit Initial Image To Output Size",
|
||||
"images": "Images",
|
||||
"images_withCount_one": "Image",
|
||||
"images_withCount_other": "Images",
|
||||
"videos_withCount_one": "Video",
|
||||
"videos_withCount_other": "Videos",
|
||||
"infillMethod": "Infill Method",
|
||||
"infillColorValue": "Fill Color",
|
||||
"info": "Info",
|
||||
"startingFrameImage": "Start Frame",
|
||||
"invoke": {
|
||||
"addingImagesTo": "Adding images to",
|
||||
"modelDisabledForTrial": "Generating with {{modelName}} is not available on trial accounts. Visit your account settings to upgrade.",
|
||||
@@ -1239,6 +1268,7 @@
|
||||
"batchNodeCollectionSizeMismatchNoGroupId": "Batch group collection size mismatch",
|
||||
"batchNodeCollectionSizeMismatch": "Collection size mismatch on Batch {{batchGroupId}}",
|
||||
"noModelSelected": "No model selected",
|
||||
"noStartingFrameImage": "No starting frame image",
|
||||
"noT5EncoderModelSelected": "No T5 Encoder model selected for FLUX generation",
|
||||
"noFLUXVAEModelSelected": "No VAE model selected for FLUX generation",
|
||||
"noCLIPEmbedModelSelected": "No CLIP Embed model selected for FLUX generation",
|
||||
@@ -1251,7 +1281,6 @@
|
||||
"modelIncompatibleScaledBboxWidth": "Scaled bbox width is {{width}} but {{model}} requires multiple of {{multiple}}",
|
||||
"modelIncompatibleScaledBboxHeight": "Scaled bbox height is {{height}} but {{model}} requires multiple of {{multiple}}",
|
||||
"fluxModelMultipleControlLoRAs": "Can only use 1 Control LoRA at a time",
|
||||
"fluxKontextMultipleReferenceImages": "Can only use 1 Reference Image at a time with FLUX Kontext via BFL API",
|
||||
"canvasIsFiltering": "Canvas is busy (filtering)",
|
||||
"canvasIsTransforming": "Canvas is busy (transforming)",
|
||||
"canvasIsRasterizing": "Canvas is busy (rasterizing)",
|
||||
@@ -1261,7 +1290,8 @@
|
||||
"noNodesInGraph": "No nodes in graph",
|
||||
"systemDisconnected": "System disconnected",
|
||||
"promptExpansionPending": "Prompt expansion in progress",
|
||||
"promptExpansionResultPending": "Please accept or discard your prompt expansion result"
|
||||
"promptExpansionResultPending": "Please accept or discard your prompt expansion result",
|
||||
"videoIsDisabled": "Video generation is not enabled for {{accountType}} accounts."
|
||||
},
|
||||
"maskBlur": "Mask Blur",
|
||||
"negativePromptPlaceholder": "Negative Prompt",
|
||||
@@ -1279,9 +1309,11 @@
|
||||
"seamlessXAxis": "Seamless X Axis",
|
||||
"seamlessYAxis": "Seamless Y Axis",
|
||||
"seed": "Seed",
|
||||
"videoActions": "Video Actions",
|
||||
"imageActions": "Image Actions",
|
||||
"sendToCanvas": "Send To Canvas",
|
||||
"sendToUpscale": "Send To Upscale",
|
||||
"sendToVideo": "Send To Video",
|
||||
"showOptionsPanel": "Show Side Panel (O or T)",
|
||||
"shuffle": "Shuffle Seed",
|
||||
"steps": "Steps",
|
||||
@@ -1293,6 +1325,7 @@
|
||||
"postProcessing": "Post-Processing (Shift + U)",
|
||||
"processImage": "Process Image",
|
||||
"upscaling": "Upscaling",
|
||||
"video": "Video",
|
||||
"useAll": "Use All",
|
||||
"useSize": "Use Size",
|
||||
"useCpuNoise": "Use CPU Noise",
|
||||
@@ -1304,6 +1337,7 @@
|
||||
"gaussianBlur": "Gaussian Blur",
|
||||
"boxBlur": "Box Blur",
|
||||
"staged": "Staged",
|
||||
"resolution": "Resolution",
|
||||
"modelDisabledForTrial": "Generating with {{modelName}} is not available on trial accounts. Visit your <LinkComponent>account settings</LinkComponent> to upgrade."
|
||||
},
|
||||
"dynamicPrompts": {
|
||||
@@ -2565,19 +2599,30 @@
|
||||
"queue": "Queue",
|
||||
"upscaling": "Upscaling",
|
||||
"upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)",
|
||||
"video": "Video",
|
||||
"gallery": "Gallery"
|
||||
},
|
||||
"panels": {
|
||||
"launchpad": "Launchpad",
|
||||
"workflowEditor": "Workflow Editor",
|
||||
"imageViewer": "Image Viewer",
|
||||
"canvas": "Canvas"
|
||||
"imageViewer": "Viewer",
|
||||
"canvas": "Canvas",
|
||||
"video": "Video"
|
||||
},
|
||||
"launchpad": {
|
||||
"workflowsTitle": "Go deep with Workflows.",
|
||||
"upscalingTitle": "Upscale and add detail.",
|
||||
"canvasTitle": "Edit and refine on Canvas.",
|
||||
"generateTitle": "Generate images from text prompts.",
|
||||
"videoTitle": "Generate videos from text prompts.",
|
||||
"video": {
|
||||
"startingFrameCalloutTitle": "Add a Starting Frame",
|
||||
"startingFrameCalloutDesc": "Add an image to control the first frame of your video."
|
||||
},
|
||||
"addStartingFrame": {
|
||||
"title": "Add a Starting Frame",
|
||||
"description": "Add an image to control the first frame of your video."
|
||||
},
|
||||
"modelGuideText": "Want to learn what prompts work best for each model?",
|
||||
"modelGuideLink": "Check out our Model Guide.",
|
||||
"createNewWorkflowFromScratch": "Create a new Workflow from scratch",
|
||||
@@ -2652,6 +2697,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"video": {
|
||||
"noVideoSelected": "No video selected",
|
||||
"selectFromGallery": "Select a video from the gallery to play"
|
||||
},
|
||||
"system": {
|
||||
"enableLogging": "Enable Logging",
|
||||
"logLevel": {
|
||||
|
||||
@@ -2,12 +2,12 @@ import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useIsRegionFocused } from 'common/hooks/focus';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import { useLoadWorkflow } from 'features/gallery/hooks/useLoadWorkflow';
|
||||
import { useRecallAll } from 'features/gallery/hooks/useRecallAll';
|
||||
import { useRecallAll } from 'features/gallery/hooks/useRecallAllImageMetadata';
|
||||
import { useRecallDimensions } from 'features/gallery/hooks/useRecallDimensions';
|
||||
import { useRecallPrompts } from 'features/gallery/hooks/useRecallPrompts';
|
||||
import { useRecallRemix } from 'features/gallery/hooks/useRecallRemix';
|
||||
import { useRecallSeed } from 'features/gallery/hooks/useRecallSeed';
|
||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||
import { selectLastSelectedItem } from 'features/gallery/store/gallerySelectors';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { memo } from 'react';
|
||||
import { useImageDTO } from 'services/api/endpoints/images';
|
||||
@@ -15,8 +15,8 @@ import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
export const GlobalImageHotkeys = memo(() => {
|
||||
useAssertSingleton('GlobalImageHotkeys');
|
||||
const imageName = useAppSelector(selectLastSelectedImage);
|
||||
const imageDTO = useImageDTO(imageName);
|
||||
const lastSelectedItem = useAppSelector(selectLastSelectedItem);
|
||||
const imageDTO = useImageDTO(lastSelectedItem?.type === 'image' ? lastSelectedItem.id : null);
|
||||
|
||||
if (!imageDTO) {
|
||||
return null;
|
||||
|
||||
@@ -3,10 +3,12 @@ import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardMo
|
||||
import { CanvasPasteModal } from 'features/controlLayers/components/CanvasPasteModal';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { DeleteImageModal } from 'features/deleteImageModal/components/DeleteImageModal';
|
||||
import { DeleteVideoModal } from 'features/deleteVideoModal/components/DeleteVideoModal';
|
||||
import { FullscreenDropzone } from 'features/dnd/FullscreenDropzone';
|
||||
import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal';
|
||||
import DeleteBoardModal from 'features/gallery/components/Boards/DeleteBoardModal';
|
||||
import { ImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
|
||||
import { ImageContextMenu } from 'features/gallery/components/ContextMenu/ImageContextMenu';
|
||||
import { VideoContextMenu } from 'features/gallery/components/ContextMenu/VideoContextMenu';
|
||||
import { ShareWorkflowModal } from 'features/nodes/components/sidePanel/workflow/WorkflowLibrary/ShareWorkflowModal';
|
||||
import { WorkflowLibraryModal } from 'features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryModal';
|
||||
import { CancelAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
|
||||
@@ -31,6 +33,7 @@ export const GlobalModalIsolator = memo(() => {
|
||||
return (
|
||||
<>
|
||||
<DeleteImageModal />
|
||||
<DeleteVideoModal />
|
||||
<ChangeBoardModal />
|
||||
<DynamicPromptsModal />
|
||||
<StylePresetModal />
|
||||
@@ -47,6 +50,7 @@ export const GlobalModalIsolator = memo(() => {
|
||||
<DeleteBoardModal />
|
||||
<GlobalImageHotkeys />
|
||||
<ImageContextMenu />
|
||||
<VideoContextMenu />
|
||||
<FullscreenDropzone />
|
||||
<VideosModal />
|
||||
<SaveWorkflowAsDialog />
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import 'i18n';
|
||||
|
||||
import type { Middleware } from '@reduxjs/toolkit';
|
||||
import type { StudioInitAction } from 'app/hooks/useStudioInitAction';
|
||||
import type { InvokeAIUIProps } from 'app/components/types';
|
||||
import { $didStudioInit } from 'app/hooks/useStudioInitAction';
|
||||
import type { LoggingOverrides } from 'app/logging/logger';
|
||||
import { $loggingOverrides, configureLogging } from 'app/logging/logger';
|
||||
import { addStorageListeners } from 'app/store/enhancers/reduxRemember/driver';
|
||||
import { $accountSettingsLink } from 'app/store/nanostores/accountSettingsLink';
|
||||
import { $accountTypeText } from 'app/store/nanostores/accountTypeText';
|
||||
import { $authToken } from 'app/store/nanostores/authToken';
|
||||
import { $baseUrl } from 'app/store/nanostores/baseUrl';
|
||||
import { $customNavComponent } from 'app/store/nanostores/customNavComponent';
|
||||
import type { CustomStarUi } from 'app/store/nanostores/customStarUI';
|
||||
import { $customStarUI } from 'app/store/nanostores/customStarUI';
|
||||
import { $isDebugging } from 'app/store/nanostores/isDebugging';
|
||||
import { $logo } from 'app/store/nanostores/logo';
|
||||
@@ -20,11 +18,10 @@ import { $projectId, $projectName, $projectUrl } from 'app/store/nanostores/proj
|
||||
import { $queueId, DEFAULT_QUEUE_ID } from 'app/store/nanostores/queueId';
|
||||
import { $store } from 'app/store/nanostores/store';
|
||||
import { $toastMap } from 'app/store/nanostores/toastMap';
|
||||
import { $videoUpsellComponent } from 'app/store/nanostores/videoUpsellComponent';
|
||||
import { $whatsNew } from 'app/store/nanostores/whatsNew';
|
||||
import { createStore } from 'app/store/store';
|
||||
import type { PartialAppConfig } from 'app/types/invokeai';
|
||||
import Loading from 'common/components/Loading/Loading';
|
||||
import type { WorkflowSortOption, WorkflowTagCategory } from 'features/nodes/store/workflowLibrarySlice';
|
||||
import {
|
||||
$workflowLibraryCategoriesOptions,
|
||||
$workflowLibrarySortOptions,
|
||||
@@ -33,47 +30,13 @@ import {
|
||||
DEFAULT_WORKFLOW_LIBRARY_SORT_OPTIONS,
|
||||
DEFAULT_WORKFLOW_LIBRARY_TAG_CATEGORIES,
|
||||
} from 'features/nodes/store/workflowLibrarySlice';
|
||||
import type { WorkflowCategory } from 'features/nodes/types/workflow';
|
||||
import type { ToastConfig } from 'features/toast/toast';
|
||||
import type { PropsWithChildren, ReactNode } from 'react';
|
||||
import React, { lazy, memo, useEffect, useLayoutEffect, useState } from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { addMiddleware, resetMiddlewares } from 'redux-dynamic-middlewares';
|
||||
import { $socketOptions } from 'services/events/stores';
|
||||
import type { ManagerOptions, SocketOptions } from 'socket.io-client';
|
||||
|
||||
const App = lazy(() => import('./App'));
|
||||
|
||||
interface Props extends PropsWithChildren {
|
||||
apiUrl?: string;
|
||||
openAPISchemaUrl?: string;
|
||||
token?: string;
|
||||
config?: PartialAppConfig;
|
||||
customNavComponent?: ReactNode;
|
||||
accountSettingsLink?: string;
|
||||
middleware?: Middleware[];
|
||||
projectId?: string;
|
||||
projectName?: string;
|
||||
projectUrl?: string;
|
||||
queueId?: string;
|
||||
studioInitAction?: StudioInitAction;
|
||||
customStarUi?: CustomStarUi;
|
||||
socketOptions?: Partial<ManagerOptions & SocketOptions>;
|
||||
isDebugging?: boolean;
|
||||
logo?: ReactNode;
|
||||
toastMap?: Record<string, ToastConfig>;
|
||||
whatsNew?: ReactNode[];
|
||||
workflowCategories?: WorkflowCategory[];
|
||||
workflowTagCategories?: WorkflowTagCategory[];
|
||||
workflowSortOptions?: WorkflowSortOption[];
|
||||
loggingOverrides?: LoggingOverrides;
|
||||
/**
|
||||
* If provided, overrides in-app navigation to the model manager
|
||||
*/
|
||||
onClickGoToModelManager?: () => void;
|
||||
storagePersistDebounce?: number;
|
||||
}
|
||||
|
||||
const InvokeAIUI = ({
|
||||
apiUrl,
|
||||
openAPISchemaUrl,
|
||||
@@ -92,6 +55,8 @@ const InvokeAIUI = ({
|
||||
isDebugging = false,
|
||||
logo,
|
||||
toastMap,
|
||||
accountTypeText,
|
||||
videoUpsellComponent,
|
||||
workflowCategories,
|
||||
workflowTagCategories,
|
||||
workflowSortOptions,
|
||||
@@ -99,7 +64,7 @@ const InvokeAIUI = ({
|
||||
onClickGoToModelManager,
|
||||
whatsNew,
|
||||
storagePersistDebounce = 300,
|
||||
}: Props) => {
|
||||
}: InvokeAIUIProps) => {
|
||||
const [store, setStore] = useState<ReturnType<typeof createStore> | undefined>(undefined);
|
||||
const [didRehydrate, setDidRehydrate] = useState(false);
|
||||
|
||||
@@ -180,6 +145,26 @@ const InvokeAIUI = ({
|
||||
};
|
||||
}, [customStarUi]);
|
||||
|
||||
useEffect(() => {
|
||||
if (accountTypeText) {
|
||||
$accountTypeText.set(accountTypeText);
|
||||
}
|
||||
|
||||
return () => {
|
||||
$accountTypeText.set('');
|
||||
};
|
||||
}, [accountTypeText]);
|
||||
|
||||
useEffect(() => {
|
||||
if (videoUpsellComponent) {
|
||||
$videoUpsellComponent.set(videoUpsellComponent);
|
||||
}
|
||||
|
||||
return () => {
|
||||
$videoUpsellComponent.set(undefined);
|
||||
};
|
||||
}, [videoUpsellComponent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (customNavComponent) {
|
||||
$customNavComponent.set(customNavComponent);
|
||||
|
||||
43
invokeai/frontend/web/src/app/components/types.ts
Normal file
43
invokeai/frontend/web/src/app/components/types.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { Middleware } from '@reduxjs/toolkit';
|
||||
import type { StudioInitAction } from 'app/hooks/useStudioInitAction';
|
||||
import type { LoggingOverrides } from 'app/logging/logger';
|
||||
import type { CustomStarUi } from 'app/store/nanostores/customStarUI';
|
||||
import type { PartialAppConfig } from 'app/types/invokeai';
|
||||
import type { SocketOptions } from 'dgram';
|
||||
import type { WorkflowSortOption, WorkflowTagCategory } from 'features/nodes/store/workflowLibrarySlice';
|
||||
import type { WorkflowCategory } from 'features/nodes/types/workflow';
|
||||
import type { ToastConfig } from 'features/toast/toast';
|
||||
import type { PropsWithChildren, ReactNode } from 'react';
|
||||
import type { ManagerOptions } from 'socket.io-client';
|
||||
|
||||
export interface InvokeAIUIProps extends PropsWithChildren {
|
||||
apiUrl?: string;
|
||||
openAPISchemaUrl?: string;
|
||||
token?: string;
|
||||
config?: PartialAppConfig;
|
||||
customNavComponent?: ReactNode;
|
||||
accountSettingsLink?: string;
|
||||
middleware?: Middleware[];
|
||||
projectId?: string;
|
||||
projectName?: string;
|
||||
projectUrl?: string;
|
||||
queueId?: string;
|
||||
studioInitAction?: StudioInitAction;
|
||||
customStarUi?: CustomStarUi;
|
||||
socketOptions?: Partial<ManagerOptions & SocketOptions>;
|
||||
isDebugging?: boolean;
|
||||
logo?: ReactNode;
|
||||
toastMap?: Record<string, ToastConfig>;
|
||||
accountTypeText?: string;
|
||||
videoUpsellComponent?: ReactNode;
|
||||
whatsNew?: ReactNode[];
|
||||
workflowCategories?: WorkflowCategory[];
|
||||
workflowTagCategories?: WorkflowTagCategory[];
|
||||
workflowSortOptions?: WorkflowSortOption[];
|
||||
loggingOverrides?: LoggingOverrides;
|
||||
/**
|
||||
* If provided, overrides in-app navigation to the model manager
|
||||
*/
|
||||
onClickGoToModelManager?: () => void;
|
||||
storagePersistDebounce?: number;
|
||||
}
|
||||
@@ -42,6 +42,7 @@ type StudioDestinationAction = _StudioInitAction<
|
||||
| 'canvas'
|
||||
| 'workflows'
|
||||
| 'upscaling'
|
||||
| 'video'
|
||||
| 'viewAllWorkflows'
|
||||
| 'viewAllWorkflowsRecommended'
|
||||
| 'viewAllStylePresets';
|
||||
@@ -118,7 +119,7 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
|
||||
const metadata = getImageMetadataResult.value;
|
||||
store.dispatch(canvasReset());
|
||||
// This shows a toast
|
||||
await MetadataUtils.recallAll(metadata, store);
|
||||
await MetadataUtils.recallAllImageMetadata(metadata, store);
|
||||
},
|
||||
[store, t]
|
||||
);
|
||||
@@ -177,6 +178,10 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
|
||||
// Go to the upscaling tab
|
||||
navigationApi.switchToTab('upscaling');
|
||||
break;
|
||||
case 'video':
|
||||
// Go to the video tab
|
||||
await navigationApi.focusPanel('video', LAUNCHPAD_PANEL_ID);
|
||||
break;
|
||||
case 'viewAllWorkflows':
|
||||
// Go to the workflows tab and open the workflow library modal
|
||||
navigationApi.switchToTab('workflows');
|
||||
|
||||
@@ -26,6 +26,7 @@ export const zLogNamespace = z.enum([
|
||||
'system',
|
||||
'queue',
|
||||
'workflows',
|
||||
'video',
|
||||
]);
|
||||
export type LogNamespace = z.infer<typeof zLogNamespace>;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import type { AppStartListening } from 'app/store/store';
|
||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { selectLastSelectedItem } from 'features/gallery/store/gallerySelectors';
|
||||
import { itemSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
|
||||
export const appStarted = createAction('app/appStarted');
|
||||
@@ -18,11 +18,13 @@ export const addAppStartedListener = (startAppListening: AppStartListening) => {
|
||||
const firstImageLoad = await take(imagesApi.endpoints.getImageNames.matchFulfilled);
|
||||
if (firstImageLoad !== null) {
|
||||
const [{ payload }] = firstImageLoad;
|
||||
const selectedImage = selectLastSelectedImage(getState());
|
||||
const selectedImage = selectLastSelectedItem(getState());
|
||||
if (selectedImage) {
|
||||
return;
|
||||
}
|
||||
dispatch(imageSelected(payload.image_names.at(0) ?? null));
|
||||
if (payload.image_names[0]) {
|
||||
dispatch(itemSelected({ type: 'image', id: payload.image_names[0] }));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { isAnyOf } from '@reduxjs/toolkit';
|
||||
import type { AppStartListening } from 'app/store/store';
|
||||
import { selectGetImageNamesQueryArgs, selectSelectedBoardId } from 'features/gallery/store/gallerySelectors';
|
||||
import { boardIdSelected, galleryViewChanged, imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
import {
|
||||
selectGalleryView,
|
||||
selectGetImageNamesQueryArgs,
|
||||
selectGetVideoIdsQueryArgs,
|
||||
selectSelectedBoardId,
|
||||
} from 'features/gallery/store/gallerySelectors';
|
||||
import { boardIdSelected, galleryViewChanged, itemSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import { videosApi } from 'services/api/endpoints/videos';
|
||||
|
||||
export const addBoardIdSelectedListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
@@ -11,35 +17,61 @@ export const addBoardIdSelectedListener = (startAppListening: AppStartListening)
|
||||
// Cancel any in-progress instances of this listener, we don't want to select an image from a previous board
|
||||
cancelActiveListeners();
|
||||
|
||||
if (boardIdSelected.match(action) && action.payload.selectedImageName) {
|
||||
// This action already has a selected image name, we trust it is valid
|
||||
if (boardIdSelected.match(action) && action.payload.select) {
|
||||
// This action already has a resource selection - skip the below auto-selection logic
|
||||
return;
|
||||
}
|
||||
|
||||
const state = getState();
|
||||
|
||||
const board_id = selectSelectedBoardId(state);
|
||||
const view = selectGalleryView(state);
|
||||
|
||||
const queryArgs = { ...selectGetImageNamesQueryArgs(state), board_id };
|
||||
if (view === 'images' || view === 'assets') {
|
||||
const queryArgs = { ...selectGetImageNamesQueryArgs(state), board_id };
|
||||
// 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
|
||||
const isSuccess = await condition(
|
||||
() => imagesApi.endpoints.getImageNames.select(queryArgs)(getState()).isSuccess,
|
||||
5000
|
||||
);
|
||||
|
||||
// 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
|
||||
const isSuccess = await condition(
|
||||
() => imagesApi.endpoints.getImageNames.select(queryArgs)(getState()).isSuccess,
|
||||
5000
|
||||
);
|
||||
if (!isSuccess) {
|
||||
dispatch(itemSelected(null));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isSuccess) {
|
||||
dispatch(imageSelected(null));
|
||||
return;
|
||||
// the board was just changed - we can select the first image
|
||||
const imageNames = imagesApi.endpoints.getImageNames.select(queryArgs)(getState()).data?.image_names;
|
||||
|
||||
const imageToSelect = imageNames && imageNames.length > 0 ? imageNames[0] : null;
|
||||
|
||||
if (imageToSelect) {
|
||||
dispatch(itemSelected({ type: 'image', id: imageToSelect }));
|
||||
}
|
||||
} else {
|
||||
const queryArgs = { ...selectGetVideoIdsQueryArgs(state), board_id };
|
||||
// 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
|
||||
const isSuccess = await condition(
|
||||
() => videosApi.endpoints.getVideoIds.select(queryArgs)(getState()).isSuccess,
|
||||
5000
|
||||
);
|
||||
|
||||
if (!isSuccess) {
|
||||
dispatch(itemSelected(null));
|
||||
return;
|
||||
}
|
||||
|
||||
// the board was just changed - we can select the first image
|
||||
const videoIds = videosApi.endpoints.getVideoIds.select(queryArgs)(getState()).data?.video_ids;
|
||||
|
||||
const videoToSelect = videoIds && videoIds.length > 0 ? videoIds[0] : null;
|
||||
|
||||
if (videoToSelect) {
|
||||
dispatch(itemSelected({ type: 'video', id: videoToSelect }));
|
||||
}
|
||||
}
|
||||
|
||||
// the board was just changed - we can select the first image
|
||||
const imageNames = imagesApi.endpoints.getImageNames.select(queryArgs)(getState()).data?.image_names;
|
||||
|
||||
const imageToSelect = imageNames?.at(0) ?? null;
|
||||
|
||||
dispatch(imageSelected(imageToSelect));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -13,12 +13,14 @@ import {
|
||||
import { refImageModelChanged, selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice';
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import { getEntityIdentifier, isFLUXReduxConfig, isIPAdapterConfig } from 'features/controlLayers/store/types';
|
||||
import { zModelIdentifierField } from 'features/nodes/types/common';
|
||||
import { modelSelected } from 'features/parameters/store/actions';
|
||||
import {
|
||||
postProcessingModelChanged,
|
||||
tileControlnetModelChanged,
|
||||
upscaleModelChanged,
|
||||
} from 'features/parameters/store/upscaleSlice';
|
||||
import { videoModelChanged } from 'features/parameters/store/videoSlice';
|
||||
import {
|
||||
zParameterCLIPEmbedModel,
|
||||
zParameterSpandrelImageToImageModel,
|
||||
@@ -41,6 +43,7 @@ import {
|
||||
isRefinerMainModelModelConfig,
|
||||
isSpandrelImageToImageModelConfig,
|
||||
isT5EncoderModelConfig,
|
||||
isVideoModelConfig,
|
||||
} from 'services/api/types';
|
||||
import type { JsonObject } from 'type-fest';
|
||||
|
||||
@@ -81,6 +84,7 @@ export const addModelsLoadedListener = (startAppListening: AppStartListening) =>
|
||||
handleCLIPEmbedModels(models, state, dispatch, log);
|
||||
handleFLUXVAEModels(models, state, dispatch, log);
|
||||
handleFLUXReduxModels(models, state, dispatch, log);
|
||||
handleVideoModels(models, state, dispatch, log);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -193,6 +197,22 @@ const handleLoRAModels: ModelHandler = (models, state, dispatch, log) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleVideoModels: ModelHandler = (models, state, dispatch, log) => {
|
||||
const videoModels = models.filter(isVideoModelConfig);
|
||||
const selectedVideoModel = state.video.videoModel;
|
||||
|
||||
if (selectedVideoModel && videoModels.some((m) => m.key === selectedVideoModel.key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstModel = videoModels[0] || null;
|
||||
if (firstModel) {
|
||||
log.debug({ firstModel }, 'No video model selected, selecting first available video model');
|
||||
dispatch(videoModelChanged({ videoModel: zModelIdentifierField.parse(firstModel) }));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const handleControlAdapterModels: ModelHandler = (models, state, dispatch, log) => {
|
||||
const caModels = models.filter(isControlLayerModelConfig);
|
||||
selectCanvasSlice(state).controlLayers.entities.forEach((entity) => {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { atom } from 'nanostores';
|
||||
|
||||
export const $accountTypeText = atom<string>('');
|
||||
@@ -0,0 +1,4 @@
|
||||
import { atom } from 'nanostores';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export const $videoUpsellComponent = atom<ReactNode | undefined>(undefined);
|
||||
@@ -33,6 +33,7 @@ import { nodesSliceConfig } from 'features/nodes/store/nodesSlice';
|
||||
import { workflowLibrarySliceConfig } from 'features/nodes/store/workflowLibrarySlice';
|
||||
import { workflowSettingsSliceConfig } from 'features/nodes/store/workflowSettingsSlice';
|
||||
import { upscaleSliceConfig } from 'features/parameters/store/upscaleSlice';
|
||||
import { videoSliceConfig } from 'features/parameters/store/videoSlice';
|
||||
import { queueSliceConfig } from 'features/queue/store/queueSlice';
|
||||
import { stylePresetSliceConfig } from 'features/stylePresets/store/stylePresetSlice';
|
||||
import { configSliceConfig } from 'features/system/store/configSlice';
|
||||
@@ -78,6 +79,7 @@ const SLICE_CONFIGS = {
|
||||
[systemSliceConfig.slice.reducerPath]: systemSliceConfig,
|
||||
[uiSliceConfig.slice.reducerPath]: uiSliceConfig,
|
||||
[upscaleSliceConfig.slice.reducerPath]: upscaleSliceConfig,
|
||||
[videoSliceConfig.slice.reducerPath]: videoSliceConfig,
|
||||
[workflowLibrarySliceConfig.slice.reducerPath]: workflowLibrarySliceConfig,
|
||||
[workflowSettingsSliceConfig.slice.reducerPath]: workflowSettingsSliceConfig,
|
||||
};
|
||||
@@ -111,6 +113,7 @@ const ALL_REDUCERS = {
|
||||
[systemSliceConfig.slice.reducerPath]: systemSliceConfig.slice.reducer,
|
||||
[uiSliceConfig.slice.reducerPath]: uiSliceConfig.slice.reducer,
|
||||
[upscaleSliceConfig.slice.reducerPath]: upscaleSliceConfig.slice.reducer,
|
||||
[videoSliceConfig.slice.reducerPath]: videoSliceConfig.slice.reducer,
|
||||
[workflowLibrarySliceConfig.slice.reducerPath]: workflowLibrarySliceConfig.slice.reducer,
|
||||
[workflowSettingsSliceConfig.slice.reducerPath]: workflowSettingsSliceConfig.slice.reducer,
|
||||
};
|
||||
|
||||
@@ -80,6 +80,7 @@ export const zAppConfig = z.object({
|
||||
allowClientSideUpload: z.boolean(),
|
||||
allowPublishWorkflows: z.boolean(),
|
||||
allowPromptExpansion: z.boolean(),
|
||||
allowVideo: z.boolean(),
|
||||
disabledTabs: z.array(zTabName),
|
||||
disabledFeatures: z.array(zAppFeature),
|
||||
disabledSDFeatures: z.array(zSDFeature),
|
||||
@@ -140,8 +141,9 @@ export const getDefaultAppConfig = (): AppConfig => ({
|
||||
allowClientSideUpload: false,
|
||||
allowPublishWorkflows: false,
|
||||
allowPromptExpansion: false,
|
||||
allowVideo: false, // used to determine if video is enabled vs upsell
|
||||
shouldShowCredits: false,
|
||||
disabledTabs: [],
|
||||
disabledTabs: ['video'], // used to determine if video functionality is visible
|
||||
disabledFeatures: ['lightbox', 'faceRestore', 'batches'] satisfies AppFeature[],
|
||||
disabledSDFeatures: ['variation', 'symmetry', 'hires', 'perlinNoise', 'noiseThreshold'] satisfies SDFeature[],
|
||||
sd: {
|
||||
|
||||
@@ -37,6 +37,7 @@ const REGION_NAMES = [
|
||||
'workflows',
|
||||
'progress',
|
||||
'settings',
|
||||
'video',
|
||||
] as const;
|
||||
/**
|
||||
* The names of the focus regions.
|
||||
|
||||
@@ -6,13 +6,13 @@ import { toast } from 'features/toast/toast';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const useDownloadImage = () => {
|
||||
export const useDownloadItem = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const authToken = useStore($authToken);
|
||||
|
||||
const downloadImage = useCallback(
|
||||
async (image_url: string, image_name: string) => {
|
||||
const downloadItem = useCallback(
|
||||
async (item_url: string, item_id: string) => {
|
||||
try {
|
||||
const requestOpts = authToken
|
||||
? {
|
||||
@@ -21,7 +21,7 @@ export const useDownloadImage = () => {
|
||||
},
|
||||
}
|
||||
: {};
|
||||
const blob = await fetch(image_url, requestOpts).then((resp) => resp.blob());
|
||||
const blob = await fetch(item_url, requestOpts).then((resp) => resp.blob());
|
||||
if (!blob) {
|
||||
throw new Error('Unable to create Blob');
|
||||
}
|
||||
@@ -30,7 +30,7 @@ export const useDownloadImage = () => {
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
a.href = url;
|
||||
a.download = image_name;
|
||||
a.download = item_id;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
@@ -47,5 +47,5 @@ export const useDownloadImage = () => {
|
||||
[t, dispatch, authToken]
|
||||
);
|
||||
|
||||
return { downloadImage };
|
||||
return { downloadItem };
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useAppStore } from 'app/store/storeHooks';
|
||||
import { useDeleteImageModalApi } from 'features/deleteImageModal/store/state';
|
||||
import { useDeleteVideoModalApi } from 'features/deleteVideoModal/store/state';
|
||||
import { selectSelection } from 'features/gallery/store/gallerySelectors';
|
||||
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
|
||||
import { useDeleteCurrentQueueItem } from 'features/queue/hooks/useDeleteCurrentQueueItem';
|
||||
@@ -12,6 +13,7 @@ import { getFocusedRegion } from './focus';
|
||||
|
||||
export const useGlobalHotkeys = () => {
|
||||
const { dispatch, getState } = useAppStore();
|
||||
const isVideoEnabled = useFeatureStatus('video');
|
||||
const isModelManagerEnabled = useFeatureStatus('modelManager');
|
||||
const queue = useInvoke();
|
||||
|
||||
@@ -92,6 +94,18 @@ export const useGlobalHotkeys = () => {
|
||||
dependencies: [dispatch],
|
||||
});
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'selectVideoTab',
|
||||
category: 'app',
|
||||
callback: () => {
|
||||
navigationApi.switchToTab('video');
|
||||
},
|
||||
options: {
|
||||
enabled: isVideoEnabled,
|
||||
},
|
||||
dependencies: [dispatch],
|
||||
});
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'selectWorkflowsTab',
|
||||
category: 'app',
|
||||
@@ -123,6 +137,8 @@ export const useGlobalHotkeys = () => {
|
||||
});
|
||||
|
||||
const deleteImageModalApi = useDeleteImageModalApi();
|
||||
const deleteVideoModalApi = useDeleteVideoModalApi();
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'deleteSelection',
|
||||
category: 'gallery',
|
||||
@@ -135,7 +151,13 @@ export const useGlobalHotkeys = () => {
|
||||
if (!selection.length) {
|
||||
return;
|
||||
}
|
||||
deleteImageModalApi.delete(selection);
|
||||
if (selection.every(({ type }) => type === 'image')) {
|
||||
deleteImageModalApi.delete(selection.map((s) => s.id));
|
||||
} else if (selection.every(({ type }) => type === 'video')) {
|
||||
deleteVideoModalApi.delete(selection.map((s) => s.id));
|
||||
} else {
|
||||
// no-op, we expect selections to always be only images or only video
|
||||
}
|
||||
},
|
||||
dependencies: [getState, deleteImageModalApi],
|
||||
});
|
||||
|
||||
@@ -13,12 +13,18 @@ import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
||||
import { useAddImagesToBoardMutation, useRemoveImagesFromBoardMutation } from 'services/api/endpoints/images';
|
||||
import { useAddVideosToBoardMutation, useRemoveVideosFromBoardMutation } from 'services/api/endpoints/videos';
|
||||
|
||||
const selectImagesToChange = createSelector(
|
||||
selectChangeBoardModalSlice,
|
||||
(changeBoardModal) => changeBoardModal.image_names
|
||||
);
|
||||
|
||||
const selectVideosToChange = createSelector(
|
||||
selectChangeBoardModalSlice,
|
||||
(changeBoardModal) => changeBoardModal.video_ids
|
||||
);
|
||||
|
||||
const selectIsModalOpen = createSelector(
|
||||
selectChangeBoardModalSlice,
|
||||
(changeBoardModal) => changeBoardModal.isModalOpen
|
||||
@@ -32,8 +38,11 @@ const ChangeBoardModal = () => {
|
||||
const { data: boards, isFetching } = useListAllBoardsQuery({ include_archived: true });
|
||||
const isModalOpen = useAppSelector(selectIsModalOpen);
|
||||
const imagesToChange = useAppSelector(selectImagesToChange);
|
||||
const videosToChange = useAppSelector(selectVideosToChange);
|
||||
const [addImagesToBoard] = useAddImagesToBoardMutation();
|
||||
const [removeImagesFromBoard] = useRemoveImagesFromBoardMutation();
|
||||
const [addVideosToBoard] = useAddVideosToBoardMutation();
|
||||
const [removeVideosFromBoard] = useRemoveVideosFromBoardMutation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const options = useMemo<ComboboxOption[]>(() => {
|
||||
@@ -57,20 +66,41 @@ const ChangeBoardModal = () => {
|
||||
}, [dispatch]);
|
||||
|
||||
const handleChangeBoard = useCallback(() => {
|
||||
if (!imagesToChange.length || !selectedBoardId) {
|
||||
if (!selectedBoardId || (imagesToChange.length === 0 && videosToChange.length === 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedBoardId === 'none') {
|
||||
removeImagesFromBoard({ image_names: imagesToChange });
|
||||
} else {
|
||||
addImagesToBoard({
|
||||
image_names: imagesToChange,
|
||||
board_id: selectedBoardId,
|
||||
});
|
||||
if (imagesToChange.length) {
|
||||
if (selectedBoardId === 'none') {
|
||||
removeImagesFromBoard({ image_names: imagesToChange });
|
||||
} else {
|
||||
addImagesToBoard({
|
||||
image_names: imagesToChange,
|
||||
board_id: selectedBoardId,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (videosToChange.length) {
|
||||
if (selectedBoardId === 'none') {
|
||||
removeVideosFromBoard({ video_ids: videosToChange });
|
||||
} else {
|
||||
addVideosToBoard({
|
||||
video_ids: videosToChange,
|
||||
board_id: selectedBoardId,
|
||||
});
|
||||
}
|
||||
}
|
||||
dispatch(changeBoardReset());
|
||||
}, [addImagesToBoard, dispatch, imagesToChange, removeImagesFromBoard, selectedBoardId]);
|
||||
}, [
|
||||
addImagesToBoard,
|
||||
dispatch,
|
||||
imagesToChange,
|
||||
videosToChange,
|
||||
removeImagesFromBoard,
|
||||
selectedBoardId,
|
||||
addVideosToBoard,
|
||||
removeVideosFromBoard,
|
||||
]);
|
||||
|
||||
const onChange = useCallback<ComboboxOnChange>((v) => {
|
||||
if (!v) {
|
||||
@@ -91,9 +121,15 @@ const ChangeBoardModal = () => {
|
||||
>
|
||||
<Flex flexDir="column" gap={4}>
|
||||
<Text>
|
||||
{t('boards.movingImagesToBoard', {
|
||||
count: imagesToChange.length,
|
||||
})}
|
||||
{imagesToChange.length > 0 &&
|
||||
t('boards.movingImagesToBoard', {
|
||||
count: imagesToChange.length,
|
||||
})}
|
||||
{videosToChange.length > 0 &&
|
||||
t('boards.movingVideosToBoard', {
|
||||
count: videosToChange.length,
|
||||
})}
|
||||
:
|
||||
</Text>
|
||||
<FormControl isDisabled={isFetching}>
|
||||
<Combobox
|
||||
|
||||
@@ -7,6 +7,7 @@ import z from 'zod';
|
||||
const zChangeBoardModalState = z.object({
|
||||
isModalOpen: z.boolean().default(false),
|
||||
image_names: z.array(z.string()).default(() => []),
|
||||
video_ids: z.array(z.string()).default(() => []),
|
||||
});
|
||||
type ChangeBoardModalState = z.infer<typeof zChangeBoardModalState>;
|
||||
|
||||
@@ -22,6 +23,9 @@ const slice = createSlice({
|
||||
imagesToChangeSelected: (state, action: PayloadAction<string[]>) => {
|
||||
state.image_names = action.payload;
|
||||
},
|
||||
videosToChangeSelected: (state, action: PayloadAction<string[]>) => {
|
||||
state.video_ids = action.payload;
|
||||
},
|
||||
changeBoardReset: (state) => {
|
||||
state.image_names = [];
|
||||
state.isModalOpen = false;
|
||||
@@ -29,7 +33,7 @@ const slice = createSlice({
|
||||
},
|
||||
});
|
||||
|
||||
export const { isModalOpenChanged, imagesToChangeSelected, changeBoardReset } = slice.actions;
|
||||
export const { isModalOpenChanged, imagesToChangeSelected, videosToChangeSelected, changeBoardReset } = slice.actions;
|
||||
|
||||
export const selectChangeBoardModalSlice = (state: RootState) => state.changeBoardModal;
|
||||
|
||||
|
||||
@@ -1611,6 +1611,7 @@ const slice = createSlice({
|
||||
state.bbox.rect.width = 1024;
|
||||
state.bbox.rect.height = 1024;
|
||||
}
|
||||
|
||||
syncScaledSize(state);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -391,7 +391,7 @@ const applyClipSkip = (state: { clipSkip: number }, model: ParameterModel | null
|
||||
|
||||
const maxClip = getModelMaxClipSkip(model);
|
||||
|
||||
state.clipSkip = clamp(clipSkip, 0, maxClip);
|
||||
state.clipSkip = clamp(clipSkip, 0, maxClip ?? 0);
|
||||
};
|
||||
|
||||
const hasModelClipSkip = (model: ParameterModel | null) => {
|
||||
@@ -399,7 +399,7 @@ const hasModelClipSkip = (model: ParameterModel | null) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
return getModelMaxClipSkip(model) > 0;
|
||||
return getModelMaxClipSkip(model) ?? 0 > 0;
|
||||
};
|
||||
|
||||
const getModelMaxClipSkip = (model: ParameterModel) => {
|
||||
@@ -408,7 +408,7 @@ const getModelMaxClipSkip = (model: ParameterModel) => {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return CLIP_SKIP_MAP[model.base].maxClip;
|
||||
return CLIP_SKIP_MAP[model.base]?.maxClip;
|
||||
};
|
||||
|
||||
const resetState = (state: ParamsState): ParamsState => {
|
||||
|
||||
@@ -482,6 +482,33 @@ export const FLUX_KONTEXT_ASPECT_RATIOS: Record<FluxKontextAspectRatio, Dimensio
|
||||
'1:1': { width: 1024, height: 1024 },
|
||||
};
|
||||
|
||||
export const zVeo3AspectRatioID = z.enum(['16:9']);
|
||||
type Veo3AspectRatio = z.infer<typeof zVeo3AspectRatioID>;
|
||||
export const isVeo3AspectRatioID = (v: unknown): v is Veo3AspectRatio => zVeo3AspectRatioID.safeParse(v).success;
|
||||
|
||||
export const zRunwayAspectRatioID = z.enum(['16:9', '4:3', '1:1', '3:4', '9:16', '21:9']);
|
||||
type RunwayAspectRatio = z.infer<typeof zRunwayAspectRatioID>;
|
||||
export const isRunwayAspectRatioID = (v: unknown): v is RunwayAspectRatio => zRunwayAspectRatioID.safeParse(v).success;
|
||||
|
||||
export const zVideoAspectRatio = z.union([zVeo3AspectRatioID, zRunwayAspectRatioID]);
|
||||
export type VideoAspectRatio = z.infer<typeof zVideoAspectRatio>;
|
||||
export const isVideoAspectRatio = (v: unknown): v is VideoAspectRatio => zVideoAspectRatio.safeParse(v).success;
|
||||
|
||||
export const zVeo3Resolution = z.enum(['720p', '1080p']);
|
||||
type Veo3Resolution = z.infer<typeof zVeo3Resolution>;
|
||||
export const isVeo3Resolution = (v: unknown): v is Veo3Resolution => zVeo3Resolution.safeParse(v).success;
|
||||
export const RESOLUTION_MAP: Record<Veo3Resolution | RunwayResolution, Dimensions> = {
|
||||
'720p': { width: 1280, height: 720 },
|
||||
'1080p': { width: 1920, height: 1080 },
|
||||
};
|
||||
|
||||
export const zRunwayResolution = z.enum(['720p']);
|
||||
type RunwayResolution = z.infer<typeof zRunwayResolution>;
|
||||
export const isRunwayResolution = (v: unknown): v is RunwayResolution => zRunwayResolution.safeParse(v).success;
|
||||
|
||||
export const zVideoResolution = z.union([zVeo3Resolution, zRunwayResolution]);
|
||||
export type VideoResolution = z.infer<typeof zVideoResolution>;
|
||||
|
||||
const zAspectRatioConfig = z.object({
|
||||
id: zAspectRatioID,
|
||||
value: z.number().gt(0),
|
||||
@@ -495,6 +522,24 @@ export const DEFAULT_ASPECT_RATIO_CONFIG: AspectRatioConfig = {
|
||||
isLocked: false,
|
||||
};
|
||||
|
||||
const zVeo3DurationID = z.enum(['8']);
|
||||
type Veo3Duration = z.infer<typeof zVeo3DurationID>;
|
||||
export const isVeo3DurationID = (v: unknown): v is Veo3Duration => zVeo3DurationID.safeParse(v).success;
|
||||
export const VEO3_DURATIONS: Record<Veo3Duration, string> = {
|
||||
'8': '8 seconds',
|
||||
};
|
||||
|
||||
const zRunwayDurationID = z.enum(['5', '10']);
|
||||
type RunwayDuration = z.infer<typeof zRunwayDurationID>;
|
||||
export const isRunwayDurationID = (v: unknown): v is RunwayDuration => zRunwayDurationID.safeParse(v).success;
|
||||
export const RUNWAY_DURATIONS: Record<RunwayDuration, string> = {
|
||||
'5': '5 seconds',
|
||||
'10': '10 seconds',
|
||||
};
|
||||
|
||||
export const zVideoDuration = z.union([zVeo3DurationID, zRunwayDurationID]);
|
||||
export type VideoDuration = z.infer<typeof zVideoDuration>;
|
||||
|
||||
const zBboxState = z.object({
|
||||
rect: z.object({
|
||||
x: z.number().int(),
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useDeleteVideoModalApi } from 'features/deleteVideoModal/store/state';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import type { VideoDTO } from 'services/api/types';
|
||||
|
||||
export const useDeleteVideo = (videoDTO?: VideoDTO | null) => {
|
||||
const deleteImageModal = useDeleteVideoModalApi();
|
||||
|
||||
const isEnabled = useMemo(() => {
|
||||
if (!videoDTO) {
|
||||
return;
|
||||
}
|
||||
return true;
|
||||
}, [videoDTO]);
|
||||
const _delete = useCallback(() => {
|
||||
if (!videoDTO) {
|
||||
return;
|
||||
}
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
deleteImageModal.delete([videoDTO.video_id]);
|
||||
}, [deleteImageModal, videoDTO, isEnabled]);
|
||||
|
||||
return {
|
||||
delete: _delete,
|
||||
isEnabled,
|
||||
};
|
||||
};
|
||||
@@ -12,7 +12,7 @@ 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 { selectGetImageNamesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { itemSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { fieldImageCollectionValueChanged, fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import type { NodesState } from 'features/nodes/store/types';
|
||||
@@ -89,9 +89,15 @@ const handleDeletions = async (image_names: string[], store: AppStore) => {
|
||||
const newImageNames = data?.image_names.filter((name) => !deleted_images.includes(name)) || [];
|
||||
const newSelectedImage = newImageNames[index ?? 0] || null;
|
||||
|
||||
if (intersection(state.gallery.selection, image_names).length > 0) {
|
||||
if (
|
||||
intersection(
|
||||
state.gallery.selection.map((s) => s.id),
|
||||
image_names
|
||||
).length > 0 &&
|
||||
newSelectedImage
|
||||
) {
|
||||
// Some selected images were deleted, clear selection
|
||||
dispatch(imageSelected(newSelectedImage));
|
||||
dispatch(itemSelected({ type: 'image', id: newSelectedImage }));
|
||||
}
|
||||
|
||||
// We need to reset the features where the image is in use - none of these work if their image(s) don't exist
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { IconButtonProps } from '@invoke-ai/ui-library';
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectSelectionCount } from 'features/gallery/store/gallerySelectors';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiTrashSimpleBold } from 'react-icons/pi';
|
||||
import { $isConnected } from 'services/events/stores';
|
||||
|
||||
type Props = Omit<IconButtonProps, 'aria-label'> & {
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
export const DeleteVideoButton = memo((props: Props) => {
|
||||
const { onClick, isDisabled } = props;
|
||||
const { t } = useTranslation();
|
||||
const isConnected = useStore($isConnected);
|
||||
const count = useAppSelector(selectSelectionCount);
|
||||
const labelMessage: string = `${t('gallery.deleteVideo', { count })} (Del)`;
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
onClick={onClick}
|
||||
icon={<PiTrashSimpleBold />}
|
||||
tooltip={labelMessage}
|
||||
aria-label={labelMessage}
|
||||
isDisabled={isDisabled || !isConnected}
|
||||
colorScheme="error"
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
DeleteVideoButton.displayName = 'DeleteVideoButton';
|
||||
@@ -0,0 +1,43 @@
|
||||
import { ConfirmationAlertDialog, Flex, FormControl, FormLabel, Switch, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { useDeleteVideoModalApi, useDeleteVideoModalState } from 'features/deleteVideoModal/store/state';
|
||||
import { selectSystemShouldConfirmOnDelete, setShouldConfirmOnDelete } from 'features/system/store/systemSlice';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const DeleteVideoModal = memo(() => {
|
||||
const state = useDeleteVideoModalState();
|
||||
const api = useDeleteVideoModalApi();
|
||||
const { dispatch } = useAppStore();
|
||||
const { t } = useTranslation();
|
||||
const shouldConfirmOnDelete = useAppSelector(selectSystemShouldConfirmOnDelete);
|
||||
|
||||
const handleChangeShouldConfirmOnDelete = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => dispatch(setShouldConfirmOnDelete(!e.target.checked)),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfirmationAlertDialog
|
||||
title={`${t('gallery.deleteVideo', { count: state.video_ids.length })}`}
|
||||
isOpen={state.isOpen}
|
||||
onClose={api.close}
|
||||
cancelButtonText={t('common.cancel')}
|
||||
acceptButtonText={t('common.delete')}
|
||||
acceptCallback={api.confirm}
|
||||
cancelCallback={api.cancel}
|
||||
useInert={false}
|
||||
>
|
||||
<Flex direction="column" gap={3}>
|
||||
<Text>{t('gallery.deleteVideoPermanent')}</Text>
|
||||
<Text>{t('common.areYouSure')}</Text>
|
||||
<FormControl>
|
||||
<FormLabel>{t('common.dontAskMeAgain')}</FormLabel>
|
||||
<Switch isChecked={!shouldConfirmOnDelete} onChange={handleChangeShouldConfirmOnDelete} />
|
||||
</FormControl>
|
||||
</Flex>
|
||||
</ConfirmationAlertDialog>
|
||||
);
|
||||
});
|
||||
DeleteVideoModal.displayName = 'DeleteVideoModal';
|
||||
@@ -0,0 +1,111 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import type { AppStore } from 'app/store/store';
|
||||
import { useAppStore } from 'app/store/storeHooks';
|
||||
import { intersection } from 'es-toolkit/compat';
|
||||
import { selectGetVideoIdsQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import { itemSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { selectSystemShouldConfirmOnDelete } from 'features/system/store/systemSlice';
|
||||
import { atom } from 'nanostores';
|
||||
import { useMemo } from 'react';
|
||||
import { videosApi } from 'services/api/endpoints/videos';
|
||||
|
||||
// Implements an awaitable modal dialog for deleting images
|
||||
|
||||
type DeleteVideosModalState = {
|
||||
video_ids: string[];
|
||||
isOpen: boolean;
|
||||
resolve?: () => void;
|
||||
reject?: (reason?: string) => void;
|
||||
};
|
||||
|
||||
const getInitialState = (): DeleteVideosModalState => ({
|
||||
video_ids: [],
|
||||
isOpen: false,
|
||||
});
|
||||
|
||||
const $deleteVideosModalState = atom<DeleteVideosModalState>(getInitialState());
|
||||
|
||||
const deleteVideosWithDialog = async (video_ids: string[], store: AppStore): Promise<void> => {
|
||||
const { getState } = store;
|
||||
const shouldConfirmOnDelete = selectSystemShouldConfirmOnDelete(getState());
|
||||
|
||||
if (!shouldConfirmOnDelete) {
|
||||
// If we don't need to confirm and the resources are not in use, delete them directly
|
||||
await handleDeletions(video_ids, store);
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
$deleteVideosModalState.set({
|
||||
video_ids,
|
||||
isOpen: true,
|
||||
resolve,
|
||||
reject,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeletions = async (video_ids: string[], store: AppStore) => {
|
||||
try {
|
||||
const { dispatch, getState } = store;
|
||||
const state = getState();
|
||||
const { data } = videosApi.endpoints.getVideoIds.select(selectGetVideoIdsQueryArgs(state))(state);
|
||||
const index = data?.video_ids.findIndex((id) => id === video_ids[0]);
|
||||
const { deleted_videos } = await dispatch(
|
||||
videosApi.endpoints.deleteVideos.initiate({ video_ids }, { track: false })
|
||||
).unwrap();
|
||||
|
||||
const newVideoIds = data?.video_ids.filter((id) => !deleted_videos.includes(id)) || [];
|
||||
const newSelectedVideoId = newVideoIds[index ?? 0] || null;
|
||||
|
||||
if (
|
||||
intersection(
|
||||
state.gallery.selection.map((s) => s.id),
|
||||
video_ids
|
||||
).length > 0 &&
|
||||
newSelectedVideoId
|
||||
) {
|
||||
// Some selected images were deleted, clear selection
|
||||
dispatch(itemSelected({ type: 'video', id: newSelectedVideoId }));
|
||||
}
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDeletion = async (store: AppStore) => {
|
||||
const state = $deleteVideosModalState.get();
|
||||
await handleDeletions(state.video_ids, store);
|
||||
state.resolve?.();
|
||||
closeSilently();
|
||||
};
|
||||
|
||||
const cancelDeletion = () => {
|
||||
const state = $deleteVideosModalState.get();
|
||||
state.reject?.('User canceled');
|
||||
closeSilently();
|
||||
};
|
||||
|
||||
const closeSilently = () => {
|
||||
$deleteVideosModalState.set(getInitialState());
|
||||
};
|
||||
|
||||
export const useDeleteVideoModalState = () => {
|
||||
const state = useStore($deleteVideosModalState);
|
||||
return state;
|
||||
};
|
||||
|
||||
export const useDeleteVideoModalApi = () => {
|
||||
const store = useAppStore();
|
||||
const api = useMemo(
|
||||
() => ({
|
||||
delete: (video_ids: string[]) => deleteVideosWithDialog(video_ids, store),
|
||||
confirm: () => confirmDeletion(store),
|
||||
cancel: cancelDeletion,
|
||||
close: closeSilently,
|
||||
}),
|
||||
[store]
|
||||
);
|
||||
|
||||
return api;
|
||||
};
|
||||
@@ -21,7 +21,7 @@ const DndDragPreviewMultipleImage = memo(({ image_names }: { image_names: string
|
||||
borderRadius="base"
|
||||
>
|
||||
<Heading>{image_names.length}</Heading>
|
||||
<Heading size="sm">{t('parameters.images')}</Heading>
|
||||
<Heading size="sm">{t('parameters.images_withCount', { count: image_names.length })}</Heading>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview';
|
||||
import { Flex, Heading } from '@invoke-ai/ui-library';
|
||||
import type { MultipleVideoDndSourceData } from 'features/dnd/dnd';
|
||||
import { DND_IMAGE_DRAG_PREVIEW_SIZE, preserveOffsetOnSourceFallbackCentered } from 'features/dnd/util';
|
||||
import { memo } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { Param0 } from 'tsafe';
|
||||
|
||||
const DndDragPreviewMultipleVideo = memo(({ video_ids }: { video_ids: string[] }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Flex
|
||||
w={DND_IMAGE_DRAG_PREVIEW_SIZE}
|
||||
h={DND_IMAGE_DRAG_PREVIEW_SIZE}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
flexDir="column"
|
||||
bg="base.900"
|
||||
borderRadius="base"
|
||||
>
|
||||
<Heading>{video_ids.length}</Heading>
|
||||
<Heading size="sm">{t('parameters.videos_withCount', { count: video_ids.length })}</Heading>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
DndDragPreviewMultipleVideo.displayName = 'DndDragPreviewMultipleVideo';
|
||||
|
||||
export type DndDragPreviewMultipleVideoState = {
|
||||
type: 'multiple-video';
|
||||
container: HTMLElement;
|
||||
video_ids: string[];
|
||||
};
|
||||
|
||||
export const createMultipleVideoDragPreview = (arg: DndDragPreviewMultipleVideoState) =>
|
||||
createPortal(<DndDragPreviewMultipleVideo video_ids={arg.video_ids} />, arg.container);
|
||||
|
||||
type SetMultipleDragPreviewArg = {
|
||||
multipleVideoDndData: MultipleVideoDndSourceData;
|
||||
setDragPreviewState: (dragPreviewState: DndDragPreviewMultipleVideoState | null) => void;
|
||||
onGenerateDragPreviewArgs: Param0<Param0<typeof draggable>['onGenerateDragPreview']>;
|
||||
};
|
||||
|
||||
export const setMultipleVideoDragPreview = ({
|
||||
multipleVideoDndData,
|
||||
onGenerateDragPreviewArgs,
|
||||
setDragPreviewState,
|
||||
}: SetMultipleDragPreviewArg) => {
|
||||
const { nativeSetDragImage, source, location } = onGenerateDragPreviewArgs;
|
||||
setCustomNativeDragPreview({
|
||||
render({ container }) {
|
||||
setDragPreviewState({ type: 'multiple-video', container, video_ids: multipleVideoDndData.payload.video_ids });
|
||||
return () => setDragPreviewState(null);
|
||||
},
|
||||
nativeSetDragImage,
|
||||
getOffset: preserveOffsetOnSourceFallbackCentered({
|
||||
element: source.element,
|
||||
input: location.current.input,
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview';
|
||||
import { chakra, Flex } from '@invoke-ai/ui-library';
|
||||
import type { SingleVideoDndSourceData } from 'features/dnd/dnd';
|
||||
import { DND_IMAGE_DRAG_PREVIEW_SIZE, preserveOffsetOnSourceFallbackCentered } from 'features/dnd/util';
|
||||
import { GalleryVideoPlaceholder } from 'features/gallery/components/ImageGrid/GalleryVideoPlaceholder';
|
||||
import { memo } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import type { VideoDTO } from 'services/api/types';
|
||||
import type { Param0 } from 'tsafe';
|
||||
|
||||
const ChakraImg = chakra('img');
|
||||
|
||||
const DndDragPreviewSingleVideo = memo(({ videoDTO }: { videoDTO: VideoDTO }) => {
|
||||
return (
|
||||
<Flex position="relative" w={DND_IMAGE_DRAG_PREVIEW_SIZE} h={DND_IMAGE_DRAG_PREVIEW_SIZE}>
|
||||
<GalleryVideoPlaceholder />
|
||||
<ChakraImg
|
||||
position="absolute"
|
||||
margin="auto"
|
||||
maxW="full"
|
||||
maxH="full"
|
||||
objectFit="contain"
|
||||
borderRadius="base"
|
||||
borderWidth={2}
|
||||
borderColor="invokeBlue.300"
|
||||
borderStyle="solid"
|
||||
cursor="grabbing"
|
||||
src={videoDTO.thumbnail_url}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
DndDragPreviewSingleVideo.displayName = 'DndDragPreviewSingleVideo';
|
||||
|
||||
export type DndDragPreviewSingleVideoState = {
|
||||
type: 'single-video';
|
||||
container: HTMLElement;
|
||||
videoDTO: VideoDTO;
|
||||
};
|
||||
|
||||
export const createSingleVideoDragPreview = (arg: DndDragPreviewSingleVideoState) =>
|
||||
createPortal(<DndDragPreviewSingleVideo videoDTO={arg.videoDTO} />, arg.container);
|
||||
|
||||
type SetSingleDragPreviewArg = {
|
||||
singleVideoDndData: SingleVideoDndSourceData;
|
||||
setDragPreviewState: (dragPreviewState: DndDragPreviewSingleVideoState | null) => void;
|
||||
onGenerateDragPreviewArgs: Param0<Param0<typeof draggable>['onGenerateDragPreview']>;
|
||||
};
|
||||
|
||||
export const setSingleVideoDragPreview = ({
|
||||
singleVideoDndData,
|
||||
onGenerateDragPreviewArgs,
|
||||
setDragPreviewState,
|
||||
}: SetSingleDragPreviewArg) => {
|
||||
const { nativeSetDragImage, source, location } = onGenerateDragPreviewArgs;
|
||||
setCustomNativeDragPreview({
|
||||
render({ container }) {
|
||||
setDragPreviewState({ type: 'single-video', container, videoDTO: singleVideoDndData.payload.videoDTO });
|
||||
return () => setDragPreviewState(null);
|
||||
},
|
||||
nativeSetDragImage,
|
||||
getOffset: preserveOffsetOnSourceFallbackCentered({
|
||||
element: source.element,
|
||||
input: location.current.input,
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -7,7 +7,7 @@ import { singleImageDndSource } from 'features/dnd/dnd';
|
||||
import type { DndDragPreviewSingleImageState } from 'features/dnd/DndDragPreviewSingleImage';
|
||||
import { createSingleImageDragPreview, setSingleImageDragPreview } from 'features/dnd/DndDragPreviewSingleImage';
|
||||
import { firefoxDndFix } from 'features/dnd/util';
|
||||
import { useImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
|
||||
import { useImageContextMenu } from 'features/gallery/components/ContextMenu/ImageContextMenu';
|
||||
import { forwardRef, memo, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
|
||||
@@ -9,9 +9,11 @@ import { selectComparisonImages } from 'features/gallery/components/ImageViewer/
|
||||
import type { BoardId } from 'features/gallery/store/types';
|
||||
import {
|
||||
addImagesToBoard,
|
||||
addVideosToBoard,
|
||||
createNewCanvasEntityFromImage,
|
||||
newCanvasFromImage,
|
||||
removeImagesFromBoard,
|
||||
removeVideosFromBoard,
|
||||
replaceCanvasEntityObjectsWithImage,
|
||||
setComparisonImage,
|
||||
setGlobalReferenceImage,
|
||||
@@ -22,9 +24,10 @@ import {
|
||||
import { fieldImageCollectionValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { selectFieldInputInstanceSafe, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { type FieldIdentifier, isImageFieldCollectionInputInstance } from 'features/nodes/types/field';
|
||||
import { startingFrameImageChanged } from 'features/parameters/store/videoSlice';
|
||||
import { expandPrompt } from 'features/prompt/PromptExpansion/expand';
|
||||
import { promptExpansionApi } from 'features/prompt/PromptExpansion/state';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import type { ImageDTO, VideoDTO } from 'services/api/types';
|
||||
import type { JsonObject } from 'type-fest';
|
||||
|
||||
const log = logger('dnd');
|
||||
@@ -70,6 +73,35 @@ type DndSource<SourceData extends DndData> = {
|
||||
typeGuard: ReturnType<typeof buildTypeGuard<SourceData>>;
|
||||
getData: ReturnType<typeof buildGetData<SourceData>>;
|
||||
};
|
||||
|
||||
//#region Single Video
|
||||
const _singleVideo = buildTypeAndKey('single-video');
|
||||
export type SingleVideoDndSourceData = DndData<
|
||||
typeof _singleVideo.type,
|
||||
typeof _singleVideo.key,
|
||||
{ videoDTO: VideoDTO }
|
||||
>;
|
||||
export const singleVideoDndSource: DndSource<SingleVideoDndSourceData> = {
|
||||
..._singleVideo,
|
||||
typeGuard: buildTypeGuard(_singleVideo.key),
|
||||
getData: buildGetData(_singleVideo.key, _singleVideo.type),
|
||||
};
|
||||
//#endregion
|
||||
|
||||
//#region Multiple Image
|
||||
const _multipleVideo = buildTypeAndKey('multiple-video');
|
||||
export type MultipleVideoDndSourceData = DndData<
|
||||
typeof _multipleVideo.type,
|
||||
typeof _multipleVideo.key,
|
||||
{ video_ids: string[]; board_id: BoardId }
|
||||
>;
|
||||
export const multipleVideoDndSource: DndSource<MultipleVideoDndSourceData> = {
|
||||
..._multipleVideo,
|
||||
typeGuard: buildTypeGuard(_multipleVideo.key),
|
||||
getData: buildGetData(_multipleVideo.key, _multipleVideo.type),
|
||||
};
|
||||
//#endregion
|
||||
|
||||
//#region Single Image
|
||||
const _singleImage = buildTypeAndKey('single-image');
|
||||
export type SingleImageDndSourceData = DndData<
|
||||
@@ -443,12 +475,22 @@ export type AddImageToBoardDndTargetData = DndData<
|
||||
>;
|
||||
export const addImageToBoardDndTarget: DndTarget<
|
||||
AddImageToBoardDndTargetData,
|
||||
SingleImageDndSourceData | MultipleImageDndSourceData
|
||||
SingleImageDndSourceData | MultipleImageDndSourceData | SingleVideoDndSourceData | MultipleVideoDndSourceData
|
||||
> = {
|
||||
..._addToBoard,
|
||||
typeGuard: buildTypeGuard(_addToBoard.key),
|
||||
getData: buildGetData(_addToBoard.key, _addToBoard.type),
|
||||
isValid: ({ sourceData, targetData }) => {
|
||||
if (singleVideoDndSource.typeGuard(sourceData)) {
|
||||
const currentBoard = sourceData.payload.videoDTO.board_id ?? 'none';
|
||||
const destinationBoard = targetData.payload.boardId;
|
||||
return currentBoard !== destinationBoard;
|
||||
}
|
||||
if (multipleVideoDndSource.typeGuard(sourceData)) {
|
||||
const currentBoard = sourceData.payload.board_id;
|
||||
const destinationBoard = targetData.payload.boardId;
|
||||
return currentBoard !== destinationBoard;
|
||||
}
|
||||
if (singleImageDndSource.typeGuard(sourceData)) {
|
||||
const currentBoard = sourceData.payload.imageDTO.board_id ?? 'none';
|
||||
const destinationBoard = targetData.payload.boardId;
|
||||
@@ -462,6 +504,18 @@ export const addImageToBoardDndTarget: DndTarget<
|
||||
return false;
|
||||
},
|
||||
handler: ({ sourceData, targetData, dispatch }) => {
|
||||
if (singleVideoDndSource.typeGuard(sourceData)) {
|
||||
const { videoDTO } = sourceData.payload;
|
||||
const { boardId } = targetData.payload;
|
||||
addVideosToBoard({ video_ids: [videoDTO.video_id], boardId, dispatch });
|
||||
}
|
||||
|
||||
if (multipleVideoDndSource.typeGuard(sourceData)) {
|
||||
const { video_ids } = sourceData.payload;
|
||||
const { boardId } = targetData.payload;
|
||||
addVideosToBoard({ video_ids, boardId, dispatch });
|
||||
}
|
||||
|
||||
if (singleImageDndSource.typeGuard(sourceData)) {
|
||||
const { imageDTO } = sourceData.payload;
|
||||
const { boardId } = targetData.payload;
|
||||
@@ -487,7 +541,7 @@ export type RemoveImageFromBoardDndTargetData = DndData<
|
||||
>;
|
||||
export const removeImageFromBoardDndTarget: DndTarget<
|
||||
RemoveImageFromBoardDndTargetData,
|
||||
SingleImageDndSourceData | MultipleImageDndSourceData
|
||||
SingleImageDndSourceData | MultipleImageDndSourceData | SingleVideoDndSourceData | MultipleVideoDndSourceData
|
||||
> = {
|
||||
..._removeFromBoard,
|
||||
typeGuard: buildTypeGuard(_removeFromBoard.key),
|
||||
@@ -503,6 +557,16 @@ export const removeImageFromBoardDndTarget: DndTarget<
|
||||
return currentBoard !== 'none';
|
||||
}
|
||||
|
||||
if (singleVideoDndSource.typeGuard(sourceData)) {
|
||||
const currentBoard = sourceData.payload.videoDTO.board_id ?? 'none';
|
||||
return currentBoard !== 'none';
|
||||
}
|
||||
|
||||
if (multipleVideoDndSource.typeGuard(sourceData)) {
|
||||
const currentBoard = sourceData.payload.board_id;
|
||||
return currentBoard !== 'none';
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
handler: ({ sourceData, dispatch }) => {
|
||||
@@ -515,6 +579,16 @@ export const removeImageFromBoardDndTarget: DndTarget<
|
||||
const { image_names } = sourceData.payload;
|
||||
removeImagesFromBoard({ image_names, dispatch });
|
||||
}
|
||||
|
||||
if (singleVideoDndSource.typeGuard(sourceData)) {
|
||||
const { videoDTO } = sourceData.payload;
|
||||
removeVideosFromBoard({ video_ids: [videoDTO.video_id], dispatch });
|
||||
}
|
||||
|
||||
if (multipleVideoDndSource.typeGuard(sourceData)) {
|
||||
const { video_ids } = sourceData.payload;
|
||||
removeVideosFromBoard({ video_ids, dispatch });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -548,6 +622,30 @@ export const promptGenerationFromImageDndTarget: DndTarget<
|
||||
};
|
||||
//#endregion
|
||||
|
||||
//#region Video Frame From Image
|
||||
const _videoFrameFromImage = buildTypeAndKey('video-frame-from-image');
|
||||
type VideoFrameFromImageDndTargetData = DndData<
|
||||
typeof _videoFrameFromImage.type,
|
||||
typeof _videoFrameFromImage.key,
|
||||
{ frame: 'start' | 'end' }
|
||||
>;
|
||||
export const videoFrameFromImageDndTarget: DndTarget<VideoFrameFromImageDndTargetData, SingleImageDndSourceData> = {
|
||||
..._videoFrameFromImage,
|
||||
typeGuard: buildTypeGuard(_videoFrameFromImage.key),
|
||||
getData: buildGetData(_videoFrameFromImage.key, _videoFrameFromImage.type),
|
||||
isValid: ({ sourceData }) => {
|
||||
if (singleImageDndSource.typeGuard(sourceData)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
handler: ({ sourceData, dispatch }) => {
|
||||
const { imageDTO } = sourceData.payload;
|
||||
dispatch(startingFrameImageChanged(imageDTOToImageWithDims(imageDTO)));
|
||||
},
|
||||
};
|
||||
//#endregion
|
||||
|
||||
export const dndTargets = [
|
||||
setGlobalReferenceImageDndTarget,
|
||||
addGlobalReferenceImageDndTarget,
|
||||
@@ -562,6 +660,7 @@ export const dndTargets = [
|
||||
addImageToBoardDndTarget,
|
||||
removeImageFromBoardDndTarget,
|
||||
promptGenerationFromImageDndTarget,
|
||||
videoFrameFromImageDndTarget,
|
||||
] as const;
|
||||
|
||||
export type AnyDndTarget = (typeof dndTargets)[number];
|
||||
|
||||
@@ -4,7 +4,13 @@ import { logger } from 'app/logging/logger';
|
||||
import { getStore } from 'app/store/nanostores/store';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import { dndTargets, multipleImageDndSource, singleImageDndSource } from 'features/dnd/dnd';
|
||||
import {
|
||||
dndTargets,
|
||||
multipleImageDndSource,
|
||||
multipleVideoDndSource,
|
||||
singleImageDndSource,
|
||||
singleVideoDndSource,
|
||||
} from 'features/dnd/dnd';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const log = logger('dnd');
|
||||
@@ -19,7 +25,12 @@ export const useDndMonitor = () => {
|
||||
const sourceData = source.data;
|
||||
|
||||
// Check for allowed sources
|
||||
if (!singleImageDndSource.typeGuard(sourceData) && !multipleImageDndSource.typeGuard(sourceData)) {
|
||||
if (
|
||||
!singleImageDndSource.typeGuard(sourceData) &&
|
||||
!multipleImageDndSource.typeGuard(sourceData) &&
|
||||
!singleVideoDndSource.typeGuard(sourceData) &&
|
||||
!multipleVideoDndSource.typeGuard(sourceData)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
import { Flex, Image, Text } from '@invoke-ai/ui-library';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useGetBoardAssetsTotalQuery, useGetBoardImagesTotalQuery } from 'services/api/endpoints/boards';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
import type { BoardDTO } from 'services/api/types';
|
||||
|
||||
type Props = {
|
||||
board: BoardDTO | null;
|
||||
boardCounts: {
|
||||
image_count: number;
|
||||
asset_count: number;
|
||||
video_count: number;
|
||||
};
|
||||
};
|
||||
|
||||
export const BoardTooltip = ({ board }: Props) => {
|
||||
export const BoardTooltip = ({ board, boardCounts }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { imagesTotal } = useGetBoardImagesTotalQuery(board?.board_id || 'none', {
|
||||
selectFromResult: ({ data }) => {
|
||||
return { imagesTotal: data?.total ?? 0 };
|
||||
},
|
||||
});
|
||||
const { assetsTotal } = useGetBoardAssetsTotalQuery(board?.board_id || 'none', {
|
||||
selectFromResult: ({ data }) => {
|
||||
return { assetsTotal: data?.total ?? 0 };
|
||||
},
|
||||
});
|
||||
const isVideoEnabled = useFeatureStatus('video');
|
||||
|
||||
const { currentData: coverImage } = useGetImageDTOQuery(board?.cover_image_name ?? skipToken);
|
||||
|
||||
return (
|
||||
@@ -39,8 +36,10 @@ export const BoardTooltip = ({ board }: Props) => {
|
||||
<Flex flexDir="column" alignItems="center">
|
||||
{board && <Text fontWeight="semibold">{board.board_name}</Text>}
|
||||
<Text noOfLines={1}>
|
||||
{t('boards.imagesWithCount', { count: imagesTotal })}, {t('boards.assetsWithCount', { count: assetsTotal })}
|
||||
{t('boards.imagesWithCount', { count: boardCounts.image_count })},{' '}
|
||||
{t('boards.assetsWithCount', { count: boardCounts.asset_count })}
|
||||
</Text>
|
||||
{isVideoEnabled && <Text noOfLines={1}>{t('boards.videosWithCount', { count: boardCounts.video_count })}</Text>}
|
||||
{board?.archived && <Text>({t('boards.archived')})</Text>}
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
selectSelectedBoardId,
|
||||
} from 'features/gallery/store/gallerySelectors';
|
||||
import { autoAddBoardIdChanged, boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArchiveBold, PiImageSquare } from 'react-icons/pi';
|
||||
@@ -36,6 +37,7 @@ const GalleryBoard = ({ board, isSelected }: GalleryBoardProps) => {
|
||||
const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
|
||||
const autoAssignBoardOnClick = useAppSelector(selectAutoAssignBoardOnClick);
|
||||
const selectedBoardId = useAppSelector(selectSelectedBoardId);
|
||||
const isVideoEnabled = useFeatureStatus('video');
|
||||
const onClick = useCallback(() => {
|
||||
if (selectedBoardId !== board.board_id) {
|
||||
dispatch(boardIdSelected({ boardId: board.board_id }));
|
||||
@@ -50,11 +52,26 @@ const GalleryBoard = ({ board, isSelected }: GalleryBoardProps) => {
|
||||
[board.board_id]
|
||||
);
|
||||
|
||||
const boardCounts = useMemo(
|
||||
() => ({
|
||||
image_count: board.image_count,
|
||||
asset_count: board.asset_count,
|
||||
video_count: board.video_count,
|
||||
}),
|
||||
[board]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box position="relative" w="full" h={12}>
|
||||
<BoardContextMenu board={board}>
|
||||
{(ref) => (
|
||||
<Tooltip label={<BoardTooltip board={board} />} openDelay={1000} placement="left" closeOnScroll p={2}>
|
||||
<Tooltip
|
||||
label={<BoardTooltip board={board} boardCounts={boardCounts} />}
|
||||
openDelay={1000}
|
||||
placement="right"
|
||||
closeOnScroll
|
||||
p={2}
|
||||
>
|
||||
<Flex
|
||||
ref={ref}
|
||||
onClick={onClick}
|
||||
@@ -71,12 +88,17 @@ const GalleryBoard = ({ board, isSelected }: GalleryBoardProps) => {
|
||||
h="full"
|
||||
>
|
||||
<CoverImage board={board} />
|
||||
<Flex w="full">
|
||||
<Flex flex={1}>
|
||||
<BoardEditableTitle board={board} isSelected={isSelected} />
|
||||
</Flex>
|
||||
{autoAddBoardId === board.board_id && <AutoAddBadge />}
|
||||
{board.archived && <Icon as={PiArchiveBold} fill="base.300" />}
|
||||
<Text variant="subtext">{board.image_count}</Text>
|
||||
<Flex justifyContent="flex-end">
|
||||
<Text variant="subtext">
|
||||
{board.image_count} | {isVideoEnabled && `${board.video_count} | `}
|
||||
{board.asset_count}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -13,9 +13,14 @@ import {
|
||||
selectBoardSearchText,
|
||||
} from 'features/gallery/store/gallerySelectors';
|
||||
import { autoAddBoardIdChanged, boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useGetBoardImagesTotalQuery } from 'services/api/endpoints/boards';
|
||||
import {
|
||||
useGetBoardAssetsTotalQuery,
|
||||
useGetBoardImagesTotalQuery,
|
||||
useGetBoardVideosTotalQuery,
|
||||
} from 'services/api/endpoints/boards';
|
||||
import { useBoardName } from 'services/api/hooks/useBoardName';
|
||||
|
||||
interface Props {
|
||||
@@ -28,11 +33,23 @@ const _hover: SystemStyleObject = {
|
||||
|
||||
const NoBoardBoard = memo(({ isSelected }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const isVideoEnabled = useFeatureStatus('video');
|
||||
const { imagesTotal } = useGetBoardImagesTotalQuery('none', {
|
||||
selectFromResult: ({ data }) => {
|
||||
return { imagesTotal: data?.total ?? 0 };
|
||||
},
|
||||
});
|
||||
const { assetsTotal } = useGetBoardAssetsTotalQuery('none', {
|
||||
selectFromResult: ({ data }) => {
|
||||
return { assetsTotal: data?.total ?? 0 };
|
||||
},
|
||||
});
|
||||
const { videoTotal } = useGetBoardVideosTotalQuery('none', {
|
||||
skip: !isVideoEnabled,
|
||||
selectFromResult: ({ data }) => {
|
||||
return { videoTotal: data?.total ?? 0 };
|
||||
},
|
||||
});
|
||||
const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
|
||||
const autoAssignBoardOnClick = useAppSelector(selectAutoAssignBoardOnClick);
|
||||
const boardSearchText = useAppSelector(selectBoardSearchText);
|
||||
@@ -56,7 +73,17 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
|
||||
<Box position="relative" w="full" h={12}>
|
||||
<NoBoardBoardContextMenu>
|
||||
{(ref) => (
|
||||
<Tooltip label={<BoardTooltip board={null} />} openDelay={1000} placement="left" closeOnScroll>
|
||||
<Tooltip
|
||||
label={
|
||||
<BoardTooltip
|
||||
board={null}
|
||||
boardCounts={{ image_count: imagesTotal, asset_count: assetsTotal, video_count: videoTotal }}
|
||||
/>
|
||||
}
|
||||
openDelay={1000}
|
||||
placement="right"
|
||||
closeOnScroll
|
||||
>
|
||||
<Flex
|
||||
ref={ref}
|
||||
onClick={handleSelectBoard}
|
||||
@@ -92,7 +119,10 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
|
||||
{boardName}
|
||||
</Text>
|
||||
{autoAddBoardId === 'none' && <AutoAddBadge />}
|
||||
<Text variant="subtext">{imagesTotal}</Text>
|
||||
<Text variant="subtext">
|
||||
{imagesTotal} | {isVideoEnabled && `${videoTotal} | `}
|
||||
{assetsTotal}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -3,8 +3,8 @@ import { Menu, MenuButton, MenuList, Portal, useGlobalMenuClose } from '@invoke-
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import MultipleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/MultipleSelectionMenuItems';
|
||||
import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems';
|
||||
import MultipleSelectionMenuItems from 'features/gallery/components/ContextMenu/MultipleSelectionMenuItems';
|
||||
import SingleSelectionMenuItems from 'features/gallery/components/ContextMenu/SingleSelectionMenuItems';
|
||||
import { selectSelectionCount } from 'features/gallery/store/gallerySelectors';
|
||||
import { map } from 'nanostores';
|
||||
import type { RefObject } from 'react';
|
||||
@@ -0,0 +1,35 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import {
|
||||
imagesToChangeSelected,
|
||||
isModalOpenChanged,
|
||||
videosToChangeSelected,
|
||||
} from 'features/changeBoardModal/store/slice';
|
||||
import { useItemDTOContext } from 'features/gallery/contexts/ItemDTOContext';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiFoldersBold } from 'react-icons/pi';
|
||||
import { isImageDTO } from 'services/api/types';
|
||||
|
||||
export const ContextMenuItemChangeBoard = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const itemDTO = useItemDTOContext();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (isImageDTO(itemDTO)) {
|
||||
dispatch(imagesToChangeSelected([itemDTO.image_name]));
|
||||
} else {
|
||||
dispatch(videosToChangeSelected([itemDTO.video_id]));
|
||||
}
|
||||
dispatch(isModalOpenChanged(true));
|
||||
}, [dispatch, itemDTO]);
|
||||
|
||||
return (
|
||||
<MenuItem icon={<PiFoldersBold />} onClickCapture={onClick}>
|
||||
{t('boards.changeBoard')}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
ContextMenuItemChangeBoard.displayName = 'ContextMenuItemChangeBoard';
|
||||
@@ -1,18 +1,23 @@
|
||||
import { IconMenuItem } from 'common/components/IconMenuItem';
|
||||
import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { useItemDTOContext } from 'features/gallery/contexts/ItemDTOContext';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCopyBold } from 'react-icons/pi';
|
||||
import { isImageDTO } from 'services/api/types';
|
||||
|
||||
export const ImageMenuItemCopy = memo(() => {
|
||||
export const ContextMenuItemCopy = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const itemDTO = useItemDTOContext();
|
||||
const copyImageToClipboard = useCopyImageToClipboard();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
copyImageToClipboard(imageDTO.image_url);
|
||||
}, [copyImageToClipboard, imageDTO.image_url]);
|
||||
if (isImageDTO(itemDTO)) {
|
||||
copyImageToClipboard(itemDTO.image_url);
|
||||
} else {
|
||||
// copyVideoToClipboard(itemDTO.video_url);
|
||||
}
|
||||
}, [copyImageToClipboard, itemDTO]);
|
||||
|
||||
return (
|
||||
<IconMenuItem
|
||||
@@ -24,4 +29,4 @@ export const ImageMenuItemCopy = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemCopy.displayName = 'ImageMenuItemCopy';
|
||||
ContextMenuItemCopy.displayName = 'ContextMenuItemCopy';
|
||||
@@ -1,22 +1,25 @@
|
||||
import { IconMenuItem } from 'common/components/IconMenuItem';
|
||||
import { useDeleteImageModalApi } from 'features/deleteImageModal/store/state';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { useItemDTOContext } from 'features/gallery/contexts/ItemDTOContext';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiTrashSimpleBold } from 'react-icons/pi';
|
||||
import { isImageDTO } from 'services/api/types';
|
||||
|
||||
export const ImageMenuItemDelete = memo(() => {
|
||||
export const ContextMenuItemDeleteImage = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const deleteImageModal = useDeleteImageModalApi();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const itemDTO = useItemDTOContext();
|
||||
|
||||
const onClick = useCallback(async () => {
|
||||
try {
|
||||
await deleteImageModal.delete([imageDTO.image_name]);
|
||||
if (isImageDTO(itemDTO)) {
|
||||
await deleteImageModal.delete([itemDTO.image_name]);
|
||||
}
|
||||
} catch {
|
||||
// noop;
|
||||
}
|
||||
}, [deleteImageModal, imageDTO]);
|
||||
}, [deleteImageModal, itemDTO]);
|
||||
|
||||
return (
|
||||
<IconMenuItem
|
||||
@@ -29,4 +32,4 @@ export const ImageMenuItemDelete = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemDelete.displayName = 'ImageMenuItemDelete';
|
||||
ContextMenuItemDeleteImage.displayName = 'ContextMenuItemDeleteImage';
|
||||
@@ -0,0 +1,35 @@
|
||||
import { IconMenuItem } from 'common/components/IconMenuItem';
|
||||
import { useDeleteVideoModalApi } from 'features/deleteVideoModal/store/state';
|
||||
import { useItemDTOContext } from 'features/gallery/contexts/ItemDTOContext';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiTrashSimpleBold } from 'react-icons/pi';
|
||||
import { isVideoDTO } from 'services/api/types';
|
||||
|
||||
export const ContextMenuItemDeleteVideo = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const deleteVideoModal = useDeleteVideoModalApi();
|
||||
const itemDTO = useItemDTOContext();
|
||||
|
||||
const onClick = useCallback(async () => {
|
||||
try {
|
||||
if (isVideoDTO(itemDTO)) {
|
||||
await deleteVideoModal.delete([itemDTO.video_id]);
|
||||
}
|
||||
} catch {
|
||||
// noop;
|
||||
}
|
||||
}, [deleteVideoModal, itemDTO]);
|
||||
|
||||
return (
|
||||
<IconMenuItem
|
||||
icon={<PiTrashSimpleBold />}
|
||||
onClickCapture={onClick}
|
||||
aria-label={t('gallery.deleteVideo', { count: 1 })}
|
||||
tooltip={t('gallery.deleteVideo', { count: 1 })}
|
||||
isDestructive
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
ContextMenuItemDeleteVideo.displayName = 'ContextMenuItemDeleteVideo';
|
||||
@@ -0,0 +1,32 @@
|
||||
import { IconMenuItem } from 'common/components/IconMenuItem';
|
||||
import { useDownloadItem } from 'common/hooks/useDownloadImage';
|
||||
import { useItemDTOContext } from 'features/gallery/contexts/ItemDTOContext';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiDownloadSimpleBold } from 'react-icons/pi';
|
||||
import { isImageDTO } from 'services/api/types';
|
||||
|
||||
export const ContextMenuItemDownload = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const itemDTO = useItemDTOContext();
|
||||
const { downloadItem } = useDownloadItem();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (isImageDTO(itemDTO)) {
|
||||
downloadItem(itemDTO.image_url, itemDTO.image_name);
|
||||
} else {
|
||||
downloadItem(itemDTO.video_url, itemDTO.video_id);
|
||||
}
|
||||
}, [downloadItem, itemDTO]);
|
||||
|
||||
return (
|
||||
<IconMenuItem
|
||||
icon={<PiDownloadSimpleBold />}
|
||||
aria-label={t('gallery.download')}
|
||||
tooltip={t('gallery.download')}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
ContextMenuItemDownload.displayName = 'ContextMenuItemDownload';
|
||||
@@ -0,0 +1,39 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useItemDTOContext } from 'features/gallery/contexts/ItemDTOContext';
|
||||
import { $hasTemplates } from 'features/nodes/store/nodesSlice';
|
||||
import { useLoadWorkflowWithDialog } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiFlowArrowBold } from 'react-icons/pi';
|
||||
import { isImageDTO } from 'services/api/types';
|
||||
|
||||
export const ContextMenuItemLoadWorkflow = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const itemDTO = useItemDTOContext();
|
||||
const loadWorkflowWithDialog = useLoadWorkflowWithDialog();
|
||||
const hasTemplates = useStore($hasTemplates);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (isImageDTO(itemDTO)) {
|
||||
loadWorkflowWithDialog({ type: 'image', data: itemDTO.image_name });
|
||||
} else {
|
||||
// loadWorkflowWithDialog({ type: 'video', data: itemDTO.video_id });
|
||||
}
|
||||
}, [loadWorkflowWithDialog, itemDTO]);
|
||||
|
||||
const isDisabled = useMemo(() => {
|
||||
if (isImageDTO(itemDTO)) {
|
||||
return !itemDTO.has_workflow || !hasTemplates;
|
||||
}
|
||||
return false;
|
||||
}, [itemDTO, hasTemplates]);
|
||||
|
||||
return (
|
||||
<MenuItem icon={<PiFlowArrowBold />} onClickCapture={onClick} isDisabled={isDisabled}>
|
||||
{t('nodes.loadWorkflow')}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
ContextMenuItemLoadWorkflow.displayName = 'ContextMenuItemLoadWorkflow';
|
||||
@@ -0,0 +1,63 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useItemDTOContext } from 'features/gallery/contexts/ItemDTOContext';
|
||||
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { IMAGE_CATEGORIES } from 'features/gallery/store/types';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
import { useGalleryPanel } from 'features/ui/layouts/use-gallery-panel';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCrosshairBold } from 'react-icons/pi';
|
||||
import { isImageDTO } from 'services/api/types';
|
||||
|
||||
export const ContextMenuItemLocateInGalery = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const itemDTO = useItemDTOContext();
|
||||
const activeTab = useAppSelector(selectActiveTab);
|
||||
const galleryPanel = useGalleryPanel(activeTab);
|
||||
|
||||
const isGalleryImage = useMemo(() => {
|
||||
return !itemDTO.is_intermediate;
|
||||
}, [itemDTO]);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
navigationApi.expandRightPanel();
|
||||
galleryPanel.expand();
|
||||
if (isImageDTO(itemDTO)) {
|
||||
flushSync(() => {
|
||||
dispatch(
|
||||
boardIdSelected({
|
||||
boardId: itemDTO.board_id ?? 'none',
|
||||
select: {
|
||||
selection: [{ type: 'image', id: itemDTO.image_name }],
|
||||
galleryView: IMAGE_CATEGORIES.includes(itemDTO.image_category) ? 'images' : 'assets',
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
} else {
|
||||
flushSync(() => {
|
||||
dispatch(
|
||||
boardIdSelected({
|
||||
boardId: itemDTO.board_id ?? 'none',
|
||||
select: {
|
||||
selection: [{ type: 'video', id: itemDTO.video_id }],
|
||||
galleryView: 'videos',
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
}, [dispatch, galleryPanel, itemDTO]);
|
||||
|
||||
return (
|
||||
<MenuItem icon={<PiCrosshairBold />} onClickCapture={onClick} isDisabled={!isGalleryImage}>
|
||||
{t('boards.locateInGalery')}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
ContextMenuItemLocateInGalery.displayName = 'ContextMenuItemLocateInGalery';
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
||||
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { useRecallAll } from 'features/gallery/hooks/useRecallAll';
|
||||
import { useItemDTOContext } from 'features/gallery/contexts/ItemDTOContext';
|
||||
import { useRecallAll } from 'features/gallery/hooks/useRecallAllImageMetadata';
|
||||
import { useRecallCLIPSkip } from 'features/gallery/hooks/useRecallCLIPSkip';
|
||||
import { useRecallDimensions } from 'features/gallery/hooks/useRecallDimensions';
|
||||
import { useRecallPrompts } from 'features/gallery/hooks/useRecallPrompts';
|
||||
@@ -17,19 +17,21 @@ import {
|
||||
PiQuotesBold,
|
||||
PiRulerBold,
|
||||
} from 'react-icons/pi';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
export const ImageMenuItemMetadataRecallActionsCanvasGenerateTabs = memo(() => {
|
||||
export const ContextMenuItemMetadataRecallActionsCanvasGenerateTabs = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const subMenu = useSubMenu();
|
||||
|
||||
const imageDTO = useImageDTOContext();
|
||||
const itemDTO = useItemDTOContext();
|
||||
|
||||
const recallAll = useRecallAll(imageDTO);
|
||||
const recallRemix = useRecallRemix(imageDTO);
|
||||
const recallPrompts = useRecallPrompts(imageDTO);
|
||||
const recallSeed = useRecallSeed(imageDTO);
|
||||
const recallDimensions = useRecallDimensions(imageDTO);
|
||||
const recallCLIPSkip = useRecallCLIPSkip(imageDTO);
|
||||
// TODO: Implement video recall metadata actions
|
||||
const recallAll = useRecallAll(itemDTO as ImageDTO);
|
||||
const recallRemix = useRecallRemix(itemDTO as ImageDTO);
|
||||
const recallPrompts = useRecallPrompts(itemDTO as ImageDTO);
|
||||
const recallSeed = useRecallSeed(itemDTO as ImageDTO);
|
||||
const recallDimensions = useRecallDimensions(itemDTO as ImageDTO);
|
||||
const recallCLIPSkip = useRecallCLIPSkip(itemDTO as ImageDTO);
|
||||
|
||||
return (
|
||||
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiArrowBendUpLeftBold />}>
|
||||
@@ -66,5 +68,5 @@ export const ImageMenuItemMetadataRecallActionsCanvasGenerateTabs = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemMetadataRecallActionsCanvasGenerateTabs.displayName =
|
||||
'ImageMenuItemMetadataRecallActionsCanvasGenerateTabs';
|
||||
ContextMenuItemMetadataRecallActionsCanvasGenerateTabs.displayName =
|
||||
'ContextMenuItemMetadataRecallActionsCanvasGenerateTabs';
|
||||
@@ -1,20 +1,22 @@
|
||||
import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
||||
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { useItemDTOContext } from 'features/gallery/contexts/ItemDTOContext';
|
||||
import { useRecallPrompts } from 'features/gallery/hooks/useRecallPrompts';
|
||||
import { useRecallSeed } from 'features/gallery/hooks/useRecallSeed';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowBendUpLeftBold, PiPlantBold, PiQuotesBold } from 'react-icons/pi';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
export const ImageMenuItemMetadataRecallActionsUpscaleTab = memo(() => {
|
||||
export const ContextMenuItemMetadataRecallActionsUpscaleTab = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const subMenu = useSubMenu();
|
||||
|
||||
const imageDTO = useImageDTOContext();
|
||||
const itemDTO = useItemDTOContext();
|
||||
|
||||
const recallPrompts = useRecallPrompts(imageDTO);
|
||||
const recallSeed = useRecallSeed(imageDTO);
|
||||
// TODO: Implement video recall metadata actions
|
||||
const recallPrompts = useRecallPrompts(itemDTO as ImageDTO);
|
||||
const recallSeed = useRecallSeed(itemDTO as ImageDTO);
|
||||
|
||||
return (
|
||||
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiArrowBendUpLeftBold />}>
|
||||
@@ -35,4 +37,4 @@ export const ImageMenuItemMetadataRecallActionsUpscaleTab = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemMetadataRecallActionsUpscaleTab.displayName = 'ImageMenuItemMetadataRecallActionsUpscaleTab';
|
||||
ContextMenuItemMetadataRecallActionsUpscaleTab.displayName = 'ContextMenuItemMetadataRecallActionsUpscaleTab';
|
||||
@@ -3,7 +3,7 @@ import { useAppStore } from 'app/store/storeHooks';
|
||||
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
|
||||
import { useCanvasIsBusySafe } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { useItemDTOContextImageOnly } from 'features/gallery/contexts/ItemDTOContext';
|
||||
import { newCanvasFromImage } from 'features/imageActions/actions';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
@@ -12,11 +12,11 @@ import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiFileBold, PiPlusBold } from 'react-icons/pi';
|
||||
|
||||
export const ImageMenuItemNewCanvasFromImageSubMenu = memo(() => {
|
||||
export const ContextMenuItemNewCanvasFromImageSubMenu = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const subMenu = useSubMenu();
|
||||
const store = useAppStore();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const imageDTO = useItemDTOContextImageOnly();
|
||||
const isBusy = useCanvasIsBusySafe();
|
||||
const isStaging = useCanvasIsStaging();
|
||||
|
||||
@@ -133,4 +133,4 @@ export const ImageMenuItemNewCanvasFromImageSubMenu = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemNewCanvasFromImageSubMenu.displayName = 'ImageMenuItemNewCanvasFromImageSubMenu';
|
||||
ContextMenuItemNewCanvasFromImageSubMenu.displayName = 'ContextMenuItemNewCanvasFromImageSubMenu';
|
||||
@@ -3,7 +3,7 @@ import { useAppStore } from 'app/store/storeHooks';
|
||||
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
|
||||
import { NewLayerIcon } from 'features/controlLayers/components/common/icons';
|
||||
import { useCanvasIsBusySafe } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { useItemDTOContextImageOnly } from 'features/gallery/contexts/ItemDTOContext';
|
||||
import { sentImageToCanvas } from 'features/gallery/store/actions';
|
||||
import { createNewCanvasEntityFromImage } from 'features/imageActions/actions';
|
||||
import { toast } from 'features/toast/toast';
|
||||
@@ -13,11 +13,11 @@ import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiPlusBold } from 'react-icons/pi';
|
||||
|
||||
export const ImageMenuItemNewLayerFromImageSubMenu = memo(() => {
|
||||
export const ContextMenuItemNewLayerFromImageSubMenu = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const subMenu = useSubMenu();
|
||||
const store = useAppStore();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const imageDTO = useItemDTOContextImageOnly();
|
||||
const isBusy = useCanvasIsBusySafe();
|
||||
|
||||
const onClickNewRasterLayerFromImage = useCallback(async () => {
|
||||
@@ -112,4 +112,4 @@ export const ImageMenuItemNewLayerFromImageSubMenu = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemNewLayerFromImageSubMenu.displayName = 'ImageMenuItemNewLayerFromImageSubMenu';
|
||||
ContextMenuItemNewLayerFromImageSubMenu.displayName = 'ContextMenuItemNewLayerFromImageSubMenu';
|
||||
@@ -1,19 +1,24 @@
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { IconMenuItem } from 'common/components/IconMenuItem';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { useItemDTOContext } from 'features/gallery/contexts/ItemDTOContext';
|
||||
import { imageOpenedInNewTab } from 'features/gallery/store/actions';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowSquareOutBold } from 'react-icons/pi';
|
||||
import { isImageDTO } from 'services/api/types';
|
||||
|
||||
export const ImageMenuItemOpenInNewTab = memo(() => {
|
||||
export const ContextMenuItemOpenInNewTab = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const itemDTO = useItemDTOContext();
|
||||
const dispatch = useAppDispatch();
|
||||
const onClick = useCallback(() => {
|
||||
window.open(imageDTO.image_url, '_blank');
|
||||
dispatch(imageOpenedInNewTab());
|
||||
}, [imageDTO.image_url, dispatch]);
|
||||
if (isImageDTO(itemDTO)) {
|
||||
window.open(itemDTO.image_url, '_blank');
|
||||
dispatch(imageOpenedInNewTab());
|
||||
} else {
|
||||
window.open(itemDTO.video_url, '_blank');
|
||||
}
|
||||
}, [itemDTO, dispatch]);
|
||||
|
||||
return (
|
||||
<IconMenuItem
|
||||
@@ -25,4 +30,4 @@ export const ImageMenuItemOpenInNewTab = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemOpenInNewTab.displayName = 'ImageMenuItemOpenInNewTab';
|
||||
ContextMenuItemOpenInNewTab.displayName = 'ContextMenuItemOpenInNewTab';
|
||||
@@ -1,22 +1,27 @@
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { IconMenuItem } from 'common/components/IconMenuItem';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { imageSelected, imageToCompareChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { useItemDTOContext } from 'features/gallery/contexts/ItemDTOContext';
|
||||
import { imageToCompareChanged, itemSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
import { VIEWER_PANEL_ID } from 'features/ui/layouts/shared';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowsOutBold } from 'react-icons/pi';
|
||||
import { isImageDTO } from 'services/api/types';
|
||||
|
||||
export const ImageMenuItemOpenInViewer = memo(() => {
|
||||
export const ContextMenuItemOpenInViewer = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const itemDTO = useItemDTOContext();
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(imageToCompareChanged(null));
|
||||
dispatch(imageSelected(imageDTO.image_name));
|
||||
navigationApi.focusPanelInActiveTab(VIEWER_PANEL_ID);
|
||||
}, [dispatch, imageDTO]);
|
||||
if (isImageDTO(itemDTO)) {
|
||||
dispatch(imageToCompareChanged(null));
|
||||
dispatch(itemSelected({ type: 'image', id: itemDTO.image_name }));
|
||||
navigationApi.focusPanelInActiveTab(VIEWER_PANEL_ID);
|
||||
} else {
|
||||
// TODO: Implement video open in viewer
|
||||
}
|
||||
}, [dispatch, itemDTO]);
|
||||
|
||||
return (
|
||||
<IconMenuItem
|
||||
@@ -28,4 +33,4 @@ export const ImageMenuItemOpenInViewer = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemOpenInViewer.displayName = 'ImageMenuItemOpenInViewer';
|
||||
ContextMenuItemOpenInViewer.displayName = 'ContextMenuItemOpenInViewer';
|
||||
@@ -1,25 +1,36 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { IconMenuItem } from 'common/components/IconMenuItem';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { useItemDTOContext } from 'features/gallery/contexts/ItemDTOContext';
|
||||
import { imageToCompareChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiImagesBold } from 'react-icons/pi';
|
||||
import { isImageDTO } from 'services/api/types';
|
||||
|
||||
export const ImageMenuItemSelectForCompare = memo(() => {
|
||||
export const ContextMenuItemSelectForCompare = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const itemDTO = useItemDTOContext();
|
||||
const selectMaySelectForCompare = useMemo(
|
||||
() => createSelector(selectGallerySlice, (gallery) => gallery.imageToCompare !== imageDTO.image_name),
|
||||
[imageDTO.image_name]
|
||||
() =>
|
||||
createSelector(selectGallerySlice, (gallery) => {
|
||||
if (isImageDTO(itemDTO)) {
|
||||
return gallery.imageToCompare !== itemDTO.image_name;
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
[itemDTO]
|
||||
);
|
||||
const maySelectForCompare = useAppSelector(selectMaySelectForCompare);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(imageToCompareChanged(imageDTO.image_name));
|
||||
}, [dispatch, imageDTO]);
|
||||
if (isImageDTO(itemDTO)) {
|
||||
dispatch(imageToCompareChanged(itemDTO.image_name));
|
||||
} else {
|
||||
// TODO: Implement video select for compare
|
||||
}
|
||||
}, [dispatch, itemDTO]);
|
||||
|
||||
return (
|
||||
<IconMenuItem
|
||||
@@ -32,4 +43,4 @@ export const ImageMenuItemSelectForCompare = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemSelectForCompare.displayName = 'ImageMenuItemSelectForCompare';
|
||||
ContextMenuItemSelectForCompare.displayName = 'ContextMenuItemSelectForCompare';
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { imageDTOToImageWithDims } from 'features/controlLayers/store/util';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { useItemDTOContextImageOnly } from 'features/gallery/contexts/ItemDTOContext';
|
||||
import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
@@ -9,10 +9,10 @@ import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiShareFatBold } from 'react-icons/pi';
|
||||
|
||||
export const ImageMenuItemSendToUpscale = memo(() => {
|
||||
export const ContextMenuItemSendToUpscale = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const imageDTO = useItemDTOContextImageOnly();
|
||||
|
||||
const handleSendToCanvas = useCallback(() => {
|
||||
dispatch(upscaleInitialImageChanged(imageDTOToImageWithDims(imageDTO)));
|
||||
@@ -31,4 +31,4 @@ export const ImageMenuItemSendToUpscale = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemSendToUpscale.displayName = 'ImageMenuItemSendToUpscale';
|
||||
ContextMenuItemSendToUpscale.displayName = 'ContextMenuItemSendToUpscale';
|
||||
@@ -0,0 +1,27 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useItemDTOContextImageOnly } from 'features/gallery/contexts/ItemDTOContext';
|
||||
import { startingFrameImageChanged } from 'features/parameters/store/videoSlice';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiVideoBold } from 'react-icons/pi';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
export const ContextMenuItemSendToVideo = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const imageDTO = useItemDTOContextImageOnly();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(startingFrameImageChanged(imageDTO));
|
||||
navigationApi.switchToTab('video');
|
||||
}, [imageDTO, dispatch]);
|
||||
|
||||
return (
|
||||
<MenuItem icon={<PiVideoBold />} onClickCapture={onClick} aria-label={t('parameters.sendToVideo')}>
|
||||
{t('parameters.sendToVideo')}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
ContextMenuItemSendToVideo.displayName = 'ContextMenuItemSendToVideo';
|
||||
@@ -1,32 +1,40 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $customStarUI } from 'app/store/nanostores/customStarUI';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { useItemDTOContext } from 'features/gallery/contexts/ItemDTOContext';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiStarBold, PiStarFill } from 'react-icons/pi';
|
||||
import { useStarImagesMutation, useUnstarImagesMutation } from 'services/api/endpoints/images';
|
||||
import { useStarVideosMutation, useUnstarVideosMutation } from 'services/api/endpoints/videos';
|
||||
import { isImageDTO, isVideoDTO } from 'services/api/types';
|
||||
|
||||
export const ImageMenuItemStarUnstar = memo(() => {
|
||||
export const ContextMenuItemStarUnstar = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const itemDTO = useItemDTOContext();
|
||||
const customStarUi = useStore($customStarUI);
|
||||
const [starImages] = useStarImagesMutation();
|
||||
const [unstarImages] = useUnstarImagesMutation();
|
||||
const [starVideos] = useStarVideosMutation();
|
||||
const [unstarVideos] = useUnstarVideosMutation();
|
||||
|
||||
const starImage = useCallback(() => {
|
||||
if (imageDTO) {
|
||||
starImages({ image_names: [imageDTO.image_name] });
|
||||
if (isImageDTO(itemDTO)) {
|
||||
starImages({ image_names: [itemDTO.image_name] });
|
||||
} else if (isVideoDTO(itemDTO)) {
|
||||
starVideos({ video_ids: [itemDTO.video_id] });
|
||||
}
|
||||
}, [starImages, imageDTO]);
|
||||
}, [starImages, itemDTO, starVideos]);
|
||||
|
||||
const unstarImage = useCallback(() => {
|
||||
if (imageDTO) {
|
||||
unstarImages({ image_names: [imageDTO.image_name] });
|
||||
if (isImageDTO(itemDTO)) {
|
||||
unstarImages({ image_names: [itemDTO.image_name] });
|
||||
} else if (isVideoDTO(itemDTO)) {
|
||||
unstarVideos({ video_ids: [itemDTO.video_id] });
|
||||
}
|
||||
}, [unstarImages, imageDTO]);
|
||||
}, [unstarImages, itemDTO, unstarVideos]);
|
||||
|
||||
if (imageDTO.starred) {
|
||||
if (itemDTO.starred) {
|
||||
return (
|
||||
<MenuItem icon={customStarUi ? customStarUi.off.icon : <PiStarFill />} onClickCapture={unstarImage}>
|
||||
{customStarUi ? customStarUi.off.text : t('gallery.unstarImage')}
|
||||
@@ -41,4 +49,4 @@ export const ImageMenuItemStarUnstar = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemStarUnstar.displayName = 'ImageMenuItemStarUnstar';
|
||||
ContextMenuItemStarUnstar.displayName = 'ContextMenuItemStarUnstar';
|
||||
@@ -1,13 +1,14 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { useItemDTOContextImageOnly } from 'features/gallery/contexts/ItemDTOContext';
|
||||
import { useCreateStylePresetFromMetadata } from 'features/gallery/hooks/useCreateStylePresetFromMetadata';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiPaintBrushBold } from 'react-icons/pi';
|
||||
|
||||
export const ImageMenuItemUseAsPromptTemplate = memo(() => {
|
||||
export const ContextMenuItemUseAsPromptTemplate = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const imageDTO = useItemDTOContextImageOnly();
|
||||
|
||||
const stylePreset = useCreateStylePresetFromMetadata(imageDTO);
|
||||
|
||||
return (
|
||||
@@ -17,4 +18,4 @@ export const ImageMenuItemUseAsPromptTemplate = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemUseAsPromptTemplate.displayName = 'ImageMenuItemUseAsPromptTemplate';
|
||||
ContextMenuItemUseAsPromptTemplate.displayName = 'ContextMenuItemUseAsPromptTemplate';
|
||||
@@ -3,16 +3,16 @@ import { useAppStore } from 'app/store/storeHooks';
|
||||
import { getDefaultRefImageConfig } from 'features/controlLayers/hooks/addLayerHooks';
|
||||
import { refImageAdded } from 'features/controlLayers/store/refImagesSlice';
|
||||
import { imageDTOToImageWithDims } from 'features/controlLayers/store/util';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { useItemDTOContextImageOnly } from 'features/gallery/contexts/ItemDTOContext';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiImageBold } from 'react-icons/pi';
|
||||
|
||||
export const ImageMenuItemUseAsRefImage = memo(() => {
|
||||
export const ContextMenuItemUseAsRefImage = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const store = useAppStore();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const imageDTO = useItemDTOContextImageOnly();
|
||||
|
||||
const onClickNewGlobalReferenceImageFromImage = useCallback(() => {
|
||||
const { dispatch, getState } = store;
|
||||
@@ -33,4 +33,4 @@ export const ImageMenuItemUseAsRefImage = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemUseAsRefImage.displayName = 'ImageMenuItemUseAsRefImage';
|
||||
ContextMenuItemUseAsRefImage.displayName = 'ContextMenuItemUseAsRefImage';
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { useItemDTOContextImageOnly } from 'features/gallery/contexts/ItemDTOContext';
|
||||
import { expandPrompt } from 'features/prompt/PromptExpansion/expand';
|
||||
import { promptExpansionApi } from 'features/prompt/PromptExpansion/state';
|
||||
import { selectAllowPromptExpansion } from 'features/system/store/configSlice';
|
||||
@@ -10,10 +10,10 @@ import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiTextTBold } from 'react-icons/pi';
|
||||
|
||||
export const ImageMenuItemUseForPromptGeneration = memo(() => {
|
||||
export const ContextMenuItemUseForPromptGeneration = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const { dispatch, getState } = useAppStore();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const imageDTO = useItemDTOContextImageOnly();
|
||||
const { isPending } = useStore(promptExpansionApi.$state);
|
||||
const isPromptExpansionEnabled = useAppSelector(selectAllowPromptExpansion);
|
||||
|
||||
@@ -43,4 +43,4 @@ export const ImageMenuItemUseForPromptGeneration = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemUseForPromptGeneration.displayName = 'ImageMenuItemUseForPromptGeneration';
|
||||
ContextMenuItemUseForPromptGeneration.displayName = 'ContextMenuItemUseForPromptGeneration';
|
||||
@@ -28,24 +28,24 @@ const MultipleSelectionMenuItems = () => {
|
||||
const [bulkDownload] = useBulkDownloadImagesMutation();
|
||||
|
||||
const handleChangeBoard = useCallback(() => {
|
||||
dispatch(imagesToChangeSelected(selection));
|
||||
dispatch(imagesToChangeSelected(selection.map((s) => s.id)));
|
||||
dispatch(isModalOpenChanged(true));
|
||||
}, [dispatch, selection]);
|
||||
|
||||
const handleDeleteSelection = useCallback(() => {
|
||||
deleteImageModal.delete(selection);
|
||||
deleteImageModal.delete(selection.map((s) => s.id));
|
||||
}, [deleteImageModal, selection]);
|
||||
|
||||
const handleStarSelection = useCallback(() => {
|
||||
starImages({ image_names: selection });
|
||||
starImages({ image_names: selection.map((s) => s.id) });
|
||||
}, [starImages, selection]);
|
||||
|
||||
const handleUnstarSelection = useCallback(() => {
|
||||
unstarImages({ image_names: selection });
|
||||
unstarImages({ image_names: selection.map((s) => s.id) });
|
||||
}, [unstarImages, selection]);
|
||||
|
||||
const handleBulkDownload = useCallback(() => {
|
||||
bulkDownload({ image_names: selection });
|
||||
bulkDownload({ image_names: selection.map((s) => s.id) });
|
||||
}, [selection, bulkDownload]);
|
||||
|
||||
return (
|
||||
@@ -0,0 +1,58 @@
|
||||
import { MenuDivider, MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $customStarUI } from 'app/store/nanostores/customStarUI';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { isModalOpenChanged, videosToChangeSelected } from 'features/changeBoardModal/store/slice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiFoldersBold, PiStarBold, PiStarFill, PiTrashSimpleBold } from 'react-icons/pi';
|
||||
import { useDeleteVideosMutation, useStarVideosMutation, useUnstarVideosMutation } from 'services/api/endpoints/videos';
|
||||
|
||||
const MultipleSelectionMenuItems = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const selection = useAppSelector((s) => s.gallery.selection);
|
||||
const customStarUi = useStore($customStarUI);
|
||||
|
||||
const [starVideos] = useStarVideosMutation();
|
||||
const [unstarVideos] = useUnstarVideosMutation();
|
||||
const [deleteVideos] = useDeleteVideosMutation();
|
||||
|
||||
const handleChangeBoard = useCallback(() => {
|
||||
dispatch(videosToChangeSelected(selection.map((s) => s.id)));
|
||||
dispatch(isModalOpenChanged(true));
|
||||
}, [dispatch, selection]);
|
||||
|
||||
const handleDeleteSelection = useCallback(() => {
|
||||
// TODO: Add confirm on delete and video usage functionality
|
||||
deleteVideos({ video_ids: selection.map((s) => s.id) });
|
||||
}, [deleteVideos, selection]);
|
||||
|
||||
const handleStarSelection = useCallback(() => {
|
||||
starVideos({ video_ids: selection.map((s) => s.id) });
|
||||
}, [starVideos, selection]);
|
||||
|
||||
const handleUnstarSelection = useCallback(() => {
|
||||
unstarVideos({ video_ids: selection.map((s) => s.id) });
|
||||
}, [unstarVideos, selection]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItem icon={customStarUi ? customStarUi.on.icon : <PiStarBold />} onClickCapture={handleUnstarSelection}>
|
||||
{customStarUi ? customStarUi.off.text : `Unstar All`}
|
||||
</MenuItem>
|
||||
<MenuItem icon={customStarUi ? customStarUi.on.icon : <PiStarFill />} onClickCapture={handleStarSelection}>
|
||||
{customStarUi ? customStarUi.on.text : `Star All`}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiFoldersBold />} onClickCapture={handleChangeBoard}>
|
||||
{t('boards.changeBoard')}
|
||||
</MenuItem>
|
||||
<MenuDivider />
|
||||
<MenuItem color="error.300" icon={<PiTrashSimpleBold />} onClickCapture={handleDeleteSelection}>
|
||||
{t('gallery.deleteSelection')}
|
||||
</MenuItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(MultipleSelectionMenuItems);
|
||||
@@ -0,0 +1,71 @@
|
||||
import { MenuDivider } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { IconMenuItemGroup } from 'common/components/IconMenuItem';
|
||||
import { ContextMenuItemChangeBoard } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemChangeBoard';
|
||||
import { ContextMenuItemCopy } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemCopy';
|
||||
import { ContextMenuItemDownload } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemDownload';
|
||||
import { ContextMenuItemLoadWorkflow } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemLoadWorkflow';
|
||||
import { ContextMenuItemLocateInGalery } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemLocateInGalery';
|
||||
import { ContextMenuItemMetadataRecallActionsCanvasGenerateTabs } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemMetadataRecallActionsCanvasGenerateTabs';
|
||||
import { ContextMenuItemNewCanvasFromImageSubMenu } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemNewCanvasFromImageSubMenu';
|
||||
import { ContextMenuItemNewLayerFromImageSubMenu } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemNewLayerFromImageSubMenu';
|
||||
import { ContextMenuItemOpenInNewTab } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemOpenInNewTab';
|
||||
import { ContextMenuItemOpenInViewer } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemOpenInViewer';
|
||||
import { ContextMenuItemSelectForCompare } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemSelectForCompare';
|
||||
import { ContextMenuItemSendToUpscale } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemSendToUpscale';
|
||||
import { ContextMenuItemSendToVideo } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemSendToVideo';
|
||||
import { ContextMenuItemStarUnstar } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemStarUnstar';
|
||||
import { ContextMenuItemUseAsPromptTemplate } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemUseAsPromptTemplate';
|
||||
import { ContextMenuItemUseAsRefImage } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemUseAsRefImage';
|
||||
import { ContextMenuItemUseForPromptGeneration } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemUseForPromptGeneration';
|
||||
import { ItemDTOContextProvider } from 'features/gallery/contexts/ItemDTOContext';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
import { ContextMenuItemDeleteImage } from './MenuItems/ContextMenuItemDeleteImage';
|
||||
import { ContextMenuItemMetadataRecallActionsUpscaleTab } from './MenuItems/ContextMenuItemMetadataRecallActionsUpscaleTab';
|
||||
|
||||
type SingleSelectionMenuItemsProps = {
|
||||
imageDTO: ImageDTO;
|
||||
};
|
||||
|
||||
const SingleSelectionMenuItems = ({ imageDTO }: SingleSelectionMenuItemsProps) => {
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
const isVideoEnabled = useFeatureStatus('video');
|
||||
|
||||
return (
|
||||
<ItemDTOContextProvider value={imageDTO}>
|
||||
<IconMenuItemGroup>
|
||||
<ContextMenuItemOpenInNewTab />
|
||||
<ContextMenuItemCopy />
|
||||
<ContextMenuItemDownload />
|
||||
<ContextMenuItemOpenInViewer />
|
||||
<ContextMenuItemSelectForCompare />
|
||||
<ContextMenuItemDeleteImage />
|
||||
</IconMenuItemGroup>
|
||||
<MenuDivider />
|
||||
<ContextMenuItemLoadWorkflow />
|
||||
{(tab === 'canvas' || tab === 'generate') && <ContextMenuItemMetadataRecallActionsCanvasGenerateTabs />}
|
||||
{tab === 'upscaling' && <ContextMenuItemMetadataRecallActionsUpscaleTab />}
|
||||
<MenuDivider />
|
||||
<ContextMenuItemSendToUpscale />
|
||||
{isVideoEnabled && <ContextMenuItemSendToVideo />}
|
||||
<ContextMenuItemUseForPromptGeneration />
|
||||
{(tab === 'canvas' || tab === 'generate') && <ContextMenuItemUseAsRefImage />}
|
||||
<ContextMenuItemUseAsPromptTemplate />
|
||||
<ContextMenuItemNewCanvasFromImageSubMenu />
|
||||
{tab === 'canvas' && <ContextMenuItemNewLayerFromImageSubMenu />}
|
||||
<MenuDivider />
|
||||
<ContextMenuItemChangeBoard />
|
||||
<ContextMenuItemStarUnstar />
|
||||
{(tab === 'canvas' || tab === 'generate' || tab === 'workflows' || tab === 'upscaling') &&
|
||||
!imageDTO.is_intermediate && (
|
||||
// Only render this button on tabs with a gallery.
|
||||
<ContextMenuItemLocateInGalery />
|
||||
)}
|
||||
</ItemDTOContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default SingleSelectionMenuItems;
|
||||
@@ -0,0 +1,33 @@
|
||||
import { MenuDivider } from '@invoke-ai/ui-library';
|
||||
import { IconMenuItemGroup } from 'common/components/IconMenuItem';
|
||||
import { ContextMenuItemChangeBoard } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemChangeBoard';
|
||||
import { ContextMenuItemDownload } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemDownload';
|
||||
import { ContextMenuItemOpenInNewTab } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemOpenInNewTab';
|
||||
import { ContextMenuItemOpenInViewer } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemOpenInViewer';
|
||||
import { ItemDTOContextProvider } from 'features/gallery/contexts/ItemDTOContext';
|
||||
import type { VideoDTO } from 'services/api/types';
|
||||
|
||||
import { ContextMenuItemDeleteVideo } from './MenuItems/ContextMenuItemDeleteVideo';
|
||||
import { ContextMenuItemStarUnstar } from './MenuItems/ContextMenuItemStarUnstar';
|
||||
|
||||
type SingleSelectionVideoMenuItemsProps = {
|
||||
videoDTO: VideoDTO;
|
||||
};
|
||||
|
||||
const SingleSelectionVideoMenuItems = ({ videoDTO }: SingleSelectionVideoMenuItemsProps) => {
|
||||
return (
|
||||
<ItemDTOContextProvider value={videoDTO}>
|
||||
<IconMenuItemGroup>
|
||||
<ContextMenuItemOpenInNewTab />
|
||||
<ContextMenuItemDownload />
|
||||
<ContextMenuItemOpenInViewer />
|
||||
<ContextMenuItemDeleteVideo />
|
||||
</IconMenuItemGroup>
|
||||
<MenuDivider />
|
||||
<ContextMenuItemStarUnstar />
|
||||
<ContextMenuItemChangeBoard />
|
||||
</ItemDTOContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default SingleSelectionVideoMenuItems;
|
||||
@@ -0,0 +1,279 @@
|
||||
import type { ChakraProps } from '@invoke-ai/ui-library';
|
||||
import { Menu, MenuButton, MenuList, Portal, useGlobalMenuClose } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import MultipleSelectionVideoMenuItems from 'features/gallery/components/ContextMenu/MultipleSelectionVideoMenuItems';
|
||||
import SingleSelectionVideoMenuItems from 'features/gallery/components/ContextMenu/SingleSelectionVideoMenuItems';
|
||||
import { selectSelectionCount } from 'features/gallery/store/gallerySelectors';
|
||||
import { map } from 'nanostores';
|
||||
import type { RefObject } from 'react';
|
||||
import { memo, useCallback, useEffect, useRef } from 'react';
|
||||
import type { VideoDTO } from 'services/api/types';
|
||||
|
||||
/**
|
||||
* The delay in milliseconds before the context menu opens on long press.
|
||||
*/
|
||||
const LONGPRESS_DELAY_MS = 500;
|
||||
/**
|
||||
* The threshold in pixels that the pointer must move before the long press is cancelled.
|
||||
*/
|
||||
const LONGPRESS_MOVE_THRESHOLD_PX = 10;
|
||||
|
||||
/**
|
||||
* The singleton state of the context menu.
|
||||
*/
|
||||
const $videoContextMenuState = map<{
|
||||
isOpen: boolean;
|
||||
videoDTO: VideoDTO | null;
|
||||
position: { x: number; y: number };
|
||||
}>({
|
||||
isOpen: false,
|
||||
videoDTO: null,
|
||||
position: { x: -1, y: -1 },
|
||||
});
|
||||
|
||||
/**
|
||||
* Convenience function to close the context menu.
|
||||
*/
|
||||
const onClose = () => {
|
||||
$videoContextMenuState.setKey('isOpen', false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Map of elements to image DTOs. This is used to determine which image DTO to show the context menu for, depending on
|
||||
* the target of the context menu or long press event.
|
||||
*/
|
||||
const elToVideoMap = new Map<HTMLElement, VideoDTO>();
|
||||
|
||||
/**
|
||||
* Given a target node, find the first registered parent element that contains the target node and return the imageDTO
|
||||
* associated with it.
|
||||
*/
|
||||
const getVideoDTOFromMap = (target: Node): VideoDTO | undefined => {
|
||||
const entry = Array.from(elToVideoMap.entries()).find((entry) => entry[0].contains(target));
|
||||
return entry?.[1];
|
||||
};
|
||||
|
||||
/**
|
||||
* Register a context menu for an image DTO on a target element.
|
||||
* @param imageDTO The image DTO to register the context menu for.
|
||||
* @param targetRef The ref of the target element that should trigger the context menu.
|
||||
*/
|
||||
export const useVideoContextMenu = (videoDTO: VideoDTO, ref: RefObject<HTMLElement> | (HTMLElement | null)) => {
|
||||
useEffect(() => {
|
||||
if (ref === null) {
|
||||
return;
|
||||
}
|
||||
const el = ref instanceof HTMLElement ? ref : ref.current;
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
elToVideoMap.set(el, videoDTO);
|
||||
return () => {
|
||||
elToVideoMap.delete(el);
|
||||
};
|
||||
}, [videoDTO, ref]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Singleton component that renders the context menu for images.
|
||||
*/
|
||||
export const VideoContextMenu = memo(() => {
|
||||
useAssertSingleton('VideoContextMenu');
|
||||
const state = useStore($videoContextMenuState);
|
||||
useGlobalMenuClose(onClose);
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Menu isOpen={state.isOpen} gutter={0} placement="auto-end" onClose={onClose}>
|
||||
<MenuButton
|
||||
aria-hidden={true}
|
||||
w={1}
|
||||
h={1}
|
||||
position="absolute"
|
||||
left={state.position.x}
|
||||
top={state.position.y}
|
||||
cursor="default"
|
||||
bg="transparent"
|
||||
_hover={_hover}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
<MenuContent />
|
||||
</Menu>
|
||||
<VideoContextMenuEventLogical />
|
||||
</Portal>
|
||||
);
|
||||
});
|
||||
|
||||
VideoContextMenu.displayName = 'VideoContextMenu';
|
||||
|
||||
const _hover: ChakraProps['_hover'] = { bg: 'transparent' };
|
||||
|
||||
/**
|
||||
* A logical component that listens for context menu events and opens the context menu. It's separate from
|
||||
* ImageContextMenu component to avoid re-rendering the whole context menu on every context menu event.
|
||||
*/
|
||||
const VideoContextMenuEventLogical = memo(() => {
|
||||
const lastPositionRef = useRef<{ x: number; y: number }>({ x: -1, y: -1 });
|
||||
const longPressTimeoutRef = useRef(0);
|
||||
const animationTimeoutRef = useRef(0);
|
||||
|
||||
const onContextMenu = useCallback((e: MouseEvent | PointerEvent) => {
|
||||
if (e.shiftKey) {
|
||||
// This is a shift + right click event, which should open the native context menu
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
const videoDTO = getVideoDTOFromMap(e.target as Node);
|
||||
|
||||
if (!videoDTO) {
|
||||
// Can't find the image DTO, close the context menu
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
// clear pending delayed open
|
||||
window.clearTimeout(animationTimeoutRef.current);
|
||||
e.preventDefault();
|
||||
|
||||
if (lastPositionRef.current.x !== e.pageX || lastPositionRef.current.y !== e.pageY) {
|
||||
// if the mouse moved, we need to close, wait for animation and reopen the menu at the new position
|
||||
if ($videoContextMenuState.get().isOpen) {
|
||||
onClose();
|
||||
}
|
||||
animationTimeoutRef.current = window.setTimeout(() => {
|
||||
// Open the menu after the animation with the new state
|
||||
$videoContextMenuState.set({
|
||||
isOpen: true,
|
||||
position: { x: e.pageX, y: e.pageY },
|
||||
videoDTO,
|
||||
});
|
||||
}, 100);
|
||||
} else {
|
||||
// else we can just open the menu at the current position w/ new state
|
||||
$videoContextMenuState.set({
|
||||
isOpen: true,
|
||||
position: { x: e.pageX, y: e.pageY },
|
||||
videoDTO,
|
||||
});
|
||||
}
|
||||
|
||||
// Always sync the last position
|
||||
lastPositionRef.current = { x: e.pageX, y: e.pageY };
|
||||
}, []);
|
||||
|
||||
// Use a long press to open the context menu on touch devices
|
||||
const onPointerDown = useCallback(
|
||||
(e: PointerEvent) => {
|
||||
if (e.pointerType === 'mouse') {
|
||||
// Bail out if it's a mouse event - this is for touch/pen only
|
||||
return;
|
||||
}
|
||||
|
||||
longPressTimeoutRef.current = window.setTimeout(() => {
|
||||
onContextMenu(e);
|
||||
}, LONGPRESS_DELAY_MS);
|
||||
|
||||
lastPositionRef.current = { x: e.pageX, y: e.pageY };
|
||||
},
|
||||
[onContextMenu]
|
||||
);
|
||||
|
||||
const onPointerMove = useCallback((e: PointerEvent) => {
|
||||
if (e.pointerType === 'mouse') {
|
||||
// Bail out if it's a mouse event - this is for touch/pen only
|
||||
return;
|
||||
}
|
||||
if (longPressTimeoutRef.current === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the pointer has moved more than the threshold, cancel the long press
|
||||
const lastPosition = lastPositionRef.current;
|
||||
|
||||
const distanceFromLastPosition = Math.hypot(e.pageX - lastPosition.x, e.pageY - lastPosition.y);
|
||||
|
||||
if (distanceFromLastPosition > LONGPRESS_MOVE_THRESHOLD_PX) {
|
||||
clearTimeout(longPressTimeoutRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onPointerUp = useCallback((e: PointerEvent) => {
|
||||
if (e.pointerType === 'mouse') {
|
||||
// Bail out if it's a mouse event - this is for touch/pen only
|
||||
return;
|
||||
}
|
||||
if (longPressTimeoutRef.current) {
|
||||
clearTimeout(longPressTimeoutRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onPointerCancel = useCallback((e: PointerEvent) => {
|
||||
if (e.pointerType === 'mouse') {
|
||||
// Bail out if it's a mouse event - this is for touch/pen only
|
||||
return;
|
||||
}
|
||||
if (longPressTimeoutRef.current) {
|
||||
clearTimeout(longPressTimeoutRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
|
||||
// Context menu events
|
||||
window.addEventListener('contextmenu', onContextMenu, { signal: controller.signal });
|
||||
|
||||
// Long press events
|
||||
window.addEventListener('pointerdown', onPointerDown, { signal: controller.signal });
|
||||
window.addEventListener('pointerup', onPointerUp, { signal: controller.signal });
|
||||
window.addEventListener('pointercancel', onPointerCancel, { signal: controller.signal });
|
||||
window.addEventListener('pointermove', onPointerMove, { signal: controller.signal });
|
||||
|
||||
return () => {
|
||||
controller.abort();
|
||||
};
|
||||
}, [onContextMenu, onPointerCancel, onPointerDown, onPointerMove, onPointerUp]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
// Clean up any timeouts when we unmount
|
||||
window.clearTimeout(animationTimeoutRef.current);
|
||||
window.clearTimeout(longPressTimeoutRef.current);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
VideoContextMenuEventLogical.displayName = 'VideoContextMenuEventLogical';
|
||||
|
||||
// The content of the context menu, which changes based on the selection count. Split out and memoized to avoid
|
||||
// re-rendering the whole context menu too often.
|
||||
const MenuContent = memo(() => {
|
||||
const selectionCount = useAppSelector(selectSelectionCount);
|
||||
const state = useStore($videoContextMenuState);
|
||||
|
||||
if (!state.videoDTO) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (selectionCount > 1) {
|
||||
return (
|
||||
<MenuList visibility="visible">
|
||||
<MultipleSelectionVideoMenuItems />
|
||||
</MenuList>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuList visibility="visible">
|
||||
<SingleSelectionVideoMenuItems videoDTO={state.videoDTO} />
|
||||
</MenuList>
|
||||
);
|
||||
});
|
||||
|
||||
MenuContent.displayName = 'MenuContent';
|
||||
@@ -6,6 +6,7 @@ import { useDisclosure } from 'common/hooks/useBoolean';
|
||||
import { useGallerySearchTerm } from 'features/gallery/components/ImageGrid/useGallerySearchTerm';
|
||||
import { selectSelectedBoardId } from 'features/gallery/store/gallerySelectors';
|
||||
import { galleryViewChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { useAutoLayoutContext } from 'features/ui/layouts/auto-layout-context';
|
||||
import { useGalleryPanel } from 'features/ui/layouts/use-gallery-panel';
|
||||
import type { CSSProperties } from 'react';
|
||||
@@ -17,7 +18,8 @@ import { useBoardName } from 'services/api/hooks/useBoardName';
|
||||
import { GallerySettingsPopover } from './GallerySettingsPopover/GallerySettingsPopover';
|
||||
import { GalleryUploadButton } from './GalleryUploadButton';
|
||||
import { GallerySearch } from './ImageGrid/GallerySearch';
|
||||
import { NewGallery } from './NewGallery';
|
||||
import { ImageGallery } from './NewGallery';
|
||||
import { VideoGallery } from './VideoGallery';
|
||||
|
||||
const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0, width: '100%' };
|
||||
|
||||
@@ -42,6 +44,10 @@ export const GalleryPanel = memo(() => {
|
||||
dispatch(galleryViewChanged('assets'));
|
||||
}, [dispatch]);
|
||||
|
||||
const handleClickVideos = useCallback(() => {
|
||||
dispatch(galleryViewChanged('videos'));
|
||||
}, [dispatch]);
|
||||
|
||||
const handleClickSearch = useCallback(() => {
|
||||
onResetSearchTerm();
|
||||
if (!searchDisclosure.isOpen && galleryPanel.$isCollapsed.get()) {
|
||||
@@ -52,6 +58,7 @@ export const GalleryPanel = memo(() => {
|
||||
|
||||
const selectedBoardId = useAppSelector(selectSelectedBoardId);
|
||||
const boardName = useBoardName(selectedBoardId);
|
||||
const isVideoEnabled = useFeatureStatus('video');
|
||||
|
||||
return (
|
||||
<Flex flexDirection="column" alignItems="center" justifyContent="space-between" h="full" w="full" minH={0}>
|
||||
@@ -75,6 +82,17 @@ export const GalleryPanel = memo(() => {
|
||||
>
|
||||
{t('parameters.images')}
|
||||
</Button>
|
||||
|
||||
{isVideoEnabled && (
|
||||
<Button
|
||||
tooltip={t('gallery.videosTab')}
|
||||
onClick={handleClickVideos}
|
||||
data-testid="videos-tab"
|
||||
colorScheme={galleryView === 'videos' ? 'invokeBlue' : undefined}
|
||||
>
|
||||
{t('gallery.videos')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
tooltip={t('gallery.assetsTab')}
|
||||
onClick={handleClickAssets}
|
||||
@@ -109,7 +127,7 @@ export const GalleryPanel = memo(() => {
|
||||
</Collapse>
|
||||
<Divider pt={2} />
|
||||
<Flex w="full" h="full" pt={2}>
|
||||
<NewGallery />
|
||||
{galleryView === 'videos' ? <VideoGallery /> : <ImageGallery />}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { imagesToChangeSelected, isModalOpenChanged } from 'features/changeBoardModal/store/slice';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiFoldersBold } from 'react-icons/pi';
|
||||
|
||||
export const ImageMenuItemChangeBoard = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const imageDTO = useImageDTOContext();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(imagesToChangeSelected([imageDTO.image_name]));
|
||||
dispatch(isModalOpenChanged(true));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
return (
|
||||
<MenuItem icon={<PiFoldersBold />} onClickCapture={onClick}>
|
||||
{t('boards.changeBoard')}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemChangeBoard.displayName = 'ImageMenuItemChangeBoard';
|
||||
@@ -1,27 +0,0 @@
|
||||
import { IconMenuItem } from 'common/components/IconMenuItem';
|
||||
import { useDownloadImage } from 'common/hooks/useDownloadImage';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiDownloadSimpleBold } from 'react-icons/pi';
|
||||
|
||||
export const ImageMenuItemDownload = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const { downloadImage } = useDownloadImage();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
downloadImage(imageDTO.image_url, imageDTO.image_name);
|
||||
}, [downloadImage, imageDTO.image_name, imageDTO.image_url]);
|
||||
|
||||
return (
|
||||
<IconMenuItem
|
||||
icon={<PiDownloadSimpleBold />}
|
||||
aria-label={t('parameters.downloadImage')}
|
||||
tooltip={t('parameters.downloadImage')}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemDownload.displayName = 'ImageMenuItemDownload';
|
||||
@@ -1,27 +0,0 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { $hasTemplates } from 'features/nodes/store/nodesSlice';
|
||||
import { useLoadWorkflowWithDialog } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiFlowArrowBold } from 'react-icons/pi';
|
||||
|
||||
export const ImageMenuItemLoadWorkflow = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const loadWorkflowWithDialog = useLoadWorkflowWithDialog();
|
||||
const hasTemplates = useStore($hasTemplates);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
loadWorkflowWithDialog({ type: 'image', data: imageDTO.image_name });
|
||||
}, [loadWorkflowWithDialog, imageDTO.image_name]);
|
||||
|
||||
return (
|
||||
<MenuItem icon={<PiFlowArrowBold />} onClickCapture={onClick} isDisabled={!imageDTO.has_workflow || !hasTemplates}>
|
||||
{t('nodes.loadWorkflow')}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemLoadWorkflow.displayName = 'ImageMenuItemLoadWorkflow';
|
||||
@@ -1,39 +0,0 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
import { useGalleryPanel } from 'features/ui/layouts/use-gallery-panel';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCrosshairBold } from 'react-icons/pi';
|
||||
|
||||
export const ImageMenuItemLocateInGalery = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const activeTab = useAppSelector(selectActiveTab);
|
||||
const galleryPanel = useGalleryPanel(activeTab);
|
||||
|
||||
const isGalleryImage = useMemo(() => {
|
||||
return !imageDTO.is_intermediate;
|
||||
}, [imageDTO]);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
navigationApi.expandRightPanel();
|
||||
galleryPanel.expand();
|
||||
flushSync(() => {
|
||||
dispatch(boardIdSelected({ boardId: imageDTO.board_id ?? 'none', selectedImageName: imageDTO.image_name }));
|
||||
});
|
||||
}, [dispatch, galleryPanel, imageDTO]);
|
||||
|
||||
return (
|
||||
<MenuItem icon={<PiCrosshairBold />} onClickCapture={onClick} isDisabled={!isGalleryImage}>
|
||||
{t('boards.locateInGalery')}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemLocateInGalery.displayName = 'ImageMenuItemLocateInGalery';
|
||||
@@ -1,68 +0,0 @@
|
||||
import { MenuDivider } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { IconMenuItemGroup } from 'common/components/IconMenuItem';
|
||||
import { ImageMenuItemChangeBoard } from 'features/gallery/components/ImageContextMenu/ImageMenuItemChangeBoard';
|
||||
import { ImageMenuItemCopy } from 'features/gallery/components/ImageContextMenu/ImageMenuItemCopy';
|
||||
import { ImageMenuItemDelete } from 'features/gallery/components/ImageContextMenu/ImageMenuItemDelete';
|
||||
import { ImageMenuItemDownload } from 'features/gallery/components/ImageContextMenu/ImageMenuItemDownload';
|
||||
import { ImageMenuItemLoadWorkflow } from 'features/gallery/components/ImageContextMenu/ImageMenuItemLoadWorkflow';
|
||||
import { ImageMenuItemLocateInGalery } from 'features/gallery/components/ImageContextMenu/ImageMenuItemLocateInGalery';
|
||||
import { ImageMenuItemMetadataRecallActionsCanvasGenerateTabs } from 'features/gallery/components/ImageContextMenu/ImageMenuItemMetadataRecallActionsCanvasGenerateTabs';
|
||||
import { ImageMenuItemNewCanvasFromImageSubMenu } from 'features/gallery/components/ImageContextMenu/ImageMenuItemNewCanvasFromImageSubMenu';
|
||||
import { ImageMenuItemNewLayerFromImageSubMenu } from 'features/gallery/components/ImageContextMenu/ImageMenuItemNewLayerFromImageSubMenu';
|
||||
import { ImageMenuItemOpenInNewTab } from 'features/gallery/components/ImageContextMenu/ImageMenuItemOpenInNewTab';
|
||||
import { ImageMenuItemOpenInViewer } from 'features/gallery/components/ImageContextMenu/ImageMenuItemOpenInViewer';
|
||||
import { ImageMenuItemSelectForCompare } from 'features/gallery/components/ImageContextMenu/ImageMenuItemSelectForCompare';
|
||||
import { ImageMenuItemSendToUpscale } from 'features/gallery/components/ImageContextMenu/ImageMenuItemSendToUpscale';
|
||||
import { ImageMenuItemStarUnstar } from 'features/gallery/components/ImageContextMenu/ImageMenuItemStarUnstar';
|
||||
import { ImageMenuItemUseAsRefImage } from 'features/gallery/components/ImageContextMenu/ImageMenuItemUseAsRefImage';
|
||||
import { ImageMenuItemUseForPromptGeneration } from 'features/gallery/components/ImageContextMenu/ImageMenuItemUseForPromptGeneration';
|
||||
import { ImageDTOContextProvider } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { memo } from 'react';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
import { ImageMenuItemMetadataRecallActionsUpscaleTab } from './ImageMenuItemMetadataRecallActionsUpscaleTab';
|
||||
import { ImageMenuItemUseAsPromptTemplate } from './ImageMenuItemUseAsPromptTemplate';
|
||||
|
||||
type SingleSelectionMenuItemsProps = {
|
||||
imageDTO: ImageDTO;
|
||||
};
|
||||
|
||||
const SingleSelectionMenuItems = ({ imageDTO }: SingleSelectionMenuItemsProps) => {
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
|
||||
return (
|
||||
<ImageDTOContextProvider value={imageDTO}>
|
||||
<IconMenuItemGroup>
|
||||
<ImageMenuItemOpenInNewTab />
|
||||
<ImageMenuItemCopy />
|
||||
<ImageMenuItemDownload />
|
||||
<ImageMenuItemOpenInViewer />
|
||||
<ImageMenuItemSelectForCompare />
|
||||
<ImageMenuItemDelete />
|
||||
</IconMenuItemGroup>
|
||||
<MenuDivider />
|
||||
<ImageMenuItemLoadWorkflow />
|
||||
{(tab === 'canvas' || tab === 'generate') && <ImageMenuItemMetadataRecallActionsCanvasGenerateTabs />}
|
||||
{tab === 'upscaling' && <ImageMenuItemMetadataRecallActionsUpscaleTab />}
|
||||
<MenuDivider />
|
||||
<ImageMenuItemSendToUpscale />
|
||||
<ImageMenuItemUseForPromptGeneration />
|
||||
{(tab === 'canvas' || tab === 'generate') && <ImageMenuItemUseAsRefImage />}
|
||||
<ImageMenuItemUseAsPromptTemplate />
|
||||
<ImageMenuItemNewCanvasFromImageSubMenu />
|
||||
{tab === 'canvas' && <ImageMenuItemNewLayerFromImageSubMenu />}
|
||||
<MenuDivider />
|
||||
<ImageMenuItemChangeBoard />
|
||||
<ImageMenuItemStarUnstar />
|
||||
{(tab === 'canvas' || tab === 'generate' || tab === 'workflows' || tab === 'upscaling') &&
|
||||
!imageDTO.is_intermediate && (
|
||||
// Only render this button on tabs with a gallery.
|
||||
<ImageMenuItemLocateInGalery />
|
||||
)}
|
||||
</ImageDTOContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(SingleSelectionMenuItems);
|
||||
@@ -1,6 +1,6 @@
|
||||
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
||||
import { draggable, monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import type { FlexProps, SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import type { FlexProps } from '@invoke-ai/ui-library';
|
||||
import { Flex, Icon, Image } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import type { AppDispatch, AppGetState } from 'app/store/store';
|
||||
@@ -12,8 +12,8 @@ import { createMultipleImageDragPreview, setMultipleImageDragPreview } from 'fea
|
||||
import type { DndDragPreviewSingleImageState } from 'features/dnd/DndDragPreviewSingleImage';
|
||||
import { createSingleImageDragPreview, setSingleImageDragPreview } from 'features/dnd/DndDragPreviewSingleImage';
|
||||
import { firefoxDndFix } from 'features/dnd/util';
|
||||
import { useImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
|
||||
import { GalleryImageHoverIcons } from 'features/gallery/components/ImageGrid/GalleryImageHoverIcons';
|
||||
import { useImageContextMenu } from 'features/gallery/components/ContextMenu/ImageContextMenu';
|
||||
import { GalleryItemHoverIcons } from 'features/gallery/components/ImageGrid/GalleryItemHoverIcons';
|
||||
import {
|
||||
selectGetImageNamesQueryArgs,
|
||||
selectSelectedBoardId,
|
||||
@@ -28,56 +28,7 @@ import { PiImageBold } from 'react-icons/pi';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
const galleryImageContainerSX = {
|
||||
containerType: 'inline-size',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
'.gallery-image-size-badge': {
|
||||
'@container (max-width: 80px)': {
|
||||
'&': { display: 'none' },
|
||||
},
|
||||
},
|
||||
'&[data-is-dragging=true]': {
|
||||
opacity: 0.3,
|
||||
},
|
||||
userSelect: 'none',
|
||||
webkitUserSelect: 'none',
|
||||
position: 'relative',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
aspectRatio: '1/1',
|
||||
'::before': {
|
||||
content: '""',
|
||||
display: 'inline-block',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
pointerEvents: 'none',
|
||||
borderRadius: 'base',
|
||||
},
|
||||
'&[data-selected=true]::before': {
|
||||
boxShadow:
|
||||
'inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-500), inset 0px 0px 0px 4px var(--invoke-colors-invokeBlue-800)',
|
||||
},
|
||||
'&[data-selected-for-compare=true]::before': {
|
||||
boxShadow:
|
||||
'inset 0px 0px 0px 3px var(--invoke-colors-invokeGreen-300), inset 0px 0px 0px 4px var(--invoke-colors-invokeGreen-800)',
|
||||
},
|
||||
'&:hover::before': {
|
||||
boxShadow:
|
||||
'inset 0px 0px 0px 1px var(--invoke-colors-invokeBlue-300), inset 0px 0px 0px 2px var(--invoke-colors-invokeBlue-800)',
|
||||
},
|
||||
'&:hover[data-selected=true]::before': {
|
||||
boxShadow:
|
||||
'inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-400), inset 0px 0px 0px 4px var(--invoke-colors-invokeBlue-800)',
|
||||
},
|
||||
'&:hover[data-selected-for-compare=true]::before': {
|
||||
boxShadow:
|
||||
'inset 0px 0px 0px 3px var(--invoke-colors-invokeGreen-200), inset 0px 0px 0px 4px var(--invoke-colors-invokeGreen-800)',
|
||||
},
|
||||
} satisfies SystemStyleObject;
|
||||
import { galleryItemContainerSX } from './galleryItemContainerSX';
|
||||
|
||||
interface Props {
|
||||
imageDTO: ImageDTO;
|
||||
@@ -95,7 +46,7 @@ const buildOnClick =
|
||||
if (imageNames.length === 0) {
|
||||
// For basic click without modifiers, we can still set selection
|
||||
if (!shiftKey && !ctrlKey && !metaKey && !altKey) {
|
||||
dispatch(selectionChanged([imageName]));
|
||||
dispatch(selectionChanged([{ type: 'image', id: imageName }]));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -110,7 +61,7 @@ const buildOnClick =
|
||||
}
|
||||
} else if (shiftKey) {
|
||||
const rangeEndImageName = imageName;
|
||||
const lastSelectedImage = selection.at(-1);
|
||||
const lastSelectedImage = selection.at(-1)?.id;
|
||||
const lastClickedIndex = imageNames.findIndex((name) => name === lastSelectedImage);
|
||||
const currentClickedIndex = imageNames.findIndex((name) => name === rangeEndImageName);
|
||||
if (lastClickedIndex > -1 && currentClickedIndex > -1) {
|
||||
@@ -121,16 +72,16 @@ const buildOnClick =
|
||||
if (currentClickedIndex < lastClickedIndex) {
|
||||
imagesToSelect.reverse();
|
||||
}
|
||||
dispatch(selectionChanged(uniq(selection.concat(imagesToSelect))));
|
||||
dispatch(selectionChanged(uniq(selection.concat(imagesToSelect.map((name) => ({ type: 'image', id: name }))))));
|
||||
}
|
||||
} else if (ctrlKey || metaKey) {
|
||||
if (selection.some((n) => n === imageName) && selection.length > 1) {
|
||||
dispatch(selectionChanged(uniq(selection.filter((n) => n !== imageName))));
|
||||
if (selection.some((n) => n.id === imageName) && selection.length > 1) {
|
||||
dispatch(selectionChanged(uniq(selection.filter((n) => n.id !== imageName))));
|
||||
} else {
|
||||
dispatch(selectionChanged(uniq(selection.concat(imageName))));
|
||||
dispatch(selectionChanged(uniq(selection.concat({ type: 'image', id: imageName }))));
|
||||
}
|
||||
} else {
|
||||
dispatch(selectionChanged([imageName]));
|
||||
dispatch(selectionChanged([{ type: 'image', id: imageName }]));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -147,7 +98,7 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
|
||||
);
|
||||
const isSelectedForCompare = useAppSelector(selectIsSelectedForCompare);
|
||||
const selectIsSelected = useMemo(
|
||||
() => createSelector(selectGallerySlice, (gallery) => gallery.selection.includes(imageDTO.image_name)),
|
||||
() => createSelector(selectGallerySlice, (gallery) => gallery.selection.some((s) => s.id === imageDTO.image_name)),
|
||||
[imageDTO.image_name]
|
||||
);
|
||||
const isSelected = useAppSelector(selectIsSelected);
|
||||
@@ -164,11 +115,12 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
|
||||
getInitialData: () => {
|
||||
const selection = selectSelection(store.getState());
|
||||
const boardId = selectSelectedBoardId(store.getState());
|
||||
|
||||
// When we have multiple images selected, and the dragged image is part of the selection, initiate a
|
||||
// multi-image drag.
|
||||
if (selection.length > 1 && selection.includes(imageDTO.image_name)) {
|
||||
if (selection.length > 1 && selection.some((s) => s.id === imageDTO.image_name)) {
|
||||
return multipleImageDndSource.getData({
|
||||
image_names: selection,
|
||||
image_names: selection.map((s) => s.id),
|
||||
board_id: boardId,
|
||||
});
|
||||
}
|
||||
@@ -244,9 +196,9 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
|
||||
<>
|
||||
<Flex
|
||||
ref={ref}
|
||||
sx={galleryImageContainerSX}
|
||||
sx={galleryItemContainerSX}
|
||||
data-is-dragging={isDragging}
|
||||
data-image-name={imageDTO.image_name}
|
||||
data-item-id={imageDTO.image_name}
|
||||
role="button"
|
||||
onMouseOver={onMouseOver}
|
||||
onMouseOut={onMouseOut}
|
||||
@@ -265,7 +217,7 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
|
||||
maxH="full"
|
||||
borderRadius="base"
|
||||
/>
|
||||
<GalleryImageHoverIcons imageDTO={imageDTO} isHovered={isHovered} />
|
||||
<GalleryItemHoverIcons itemDTO={imageDTO} isHovered={isHovered} />
|
||||
</Flex>
|
||||
{dragPreviewState?.type === 'multiple-image' ? createMultipleImageDragPreview(dragPreviewState) : null}
|
||||
{dragPreviewState?.type === 'single-image' ? createSingleImageDragPreview(dragPreviewState) : null}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { GalleryImageDeleteIconButton } from 'features/gallery/components/ImageGrid/GalleryImageDeleteIconButton';
|
||||
import { GalleryImageOpenInViewerIconButton } from 'features/gallery/components/ImageGrid/GalleryImageOpenInViewerIconButton';
|
||||
import { GalleryImageSizeBadge } from 'features/gallery/components/ImageGrid/GalleryImageSizeBadge';
|
||||
import { GalleryImageStarIconButton } from 'features/gallery/components/ImageGrid/GalleryImageStarIconButton';
|
||||
import { selectAlwaysShouldImageSizeBadge } from 'features/gallery/store/gallerySelectors';
|
||||
import { memo } from 'react';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
type Props = {
|
||||
imageDTO: ImageDTO;
|
||||
isHovered: boolean;
|
||||
};
|
||||
|
||||
export const GalleryImageHoverIcons = memo(({ imageDTO, isHovered }: Props) => {
|
||||
const alwaysShowImageSizeBadge = useAppSelector(selectAlwaysShouldImageSizeBadge);
|
||||
|
||||
return (
|
||||
<>
|
||||
{(isHovered || alwaysShowImageSizeBadge) && <GalleryImageSizeBadge imageDTO={imageDTO} />}
|
||||
{(isHovered || imageDTO.starred) && <GalleryImageStarIconButton imageDTO={imageDTO} />}
|
||||
{isHovered && <GalleryImageDeleteIconButton imageDTO={imageDTO} />}
|
||||
{isHovered && <GalleryImageOpenInViewerIconButton imageDTO={imageDTO} />}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
GalleryImageHoverIcons.displayName = 'GalleryImageHoverIcons';
|
||||
@@ -1,51 +0,0 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $customStarUI } from 'app/store/nanostores/customStarUI';
|
||||
import { DndImageIcon } from 'features/dnd/DndImageIcon';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { PiStarBold, PiStarFill } from 'react-icons/pi';
|
||||
import { useStarImagesMutation, useUnstarImagesMutation } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
type Props = {
|
||||
imageDTO: ImageDTO;
|
||||
};
|
||||
|
||||
export const GalleryImageStarIconButton = memo(({ imageDTO }: Props) => {
|
||||
const customStarUi = useStore($customStarUI);
|
||||
const [starImages] = useStarImagesMutation();
|
||||
const [unstarImages] = useUnstarImagesMutation();
|
||||
|
||||
const toggleStarredState = useCallback(() => {
|
||||
if (imageDTO.starred) {
|
||||
unstarImages({ image_names: [imageDTO.image_name] });
|
||||
} else {
|
||||
starImages({ image_names: [imageDTO.image_name] });
|
||||
}
|
||||
}, [starImages, unstarImages, imageDTO]);
|
||||
|
||||
if (customStarUi) {
|
||||
return (
|
||||
<DndImageIcon
|
||||
onClick={toggleStarredState}
|
||||
icon={imageDTO.starred ? customStarUi.on.icon : customStarUi.off.icon}
|
||||
tooltip={imageDTO.starred ? customStarUi.on.text : customStarUi.off.text}
|
||||
position="absolute"
|
||||
top={2}
|
||||
insetInlineEnd={2}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DndImageIcon
|
||||
onClick={toggleStarredState}
|
||||
icon={imageDTO.starred ? <PiStarFill /> : <PiStarBold />}
|
||||
tooltip={imageDTO.starred ? 'Unstar' : 'Star'}
|
||||
position="absolute"
|
||||
top={2}
|
||||
insetInlineEnd={2}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
GalleryImageStarIconButton.displayName = 'GalleryImageStarIconButton';
|
||||
@@ -5,26 +5,33 @@ import type { MouseEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiTrashSimpleFill } from 'react-icons/pi';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import { useDeleteVideosMutation } from 'services/api/endpoints/videos';
|
||||
import { type ImageDTO, isImageDTO, type VideoDTO } from 'services/api/types';
|
||||
|
||||
type Props = {
|
||||
imageDTO: ImageDTO;
|
||||
itemDTO: ImageDTO | VideoDTO;
|
||||
};
|
||||
|
||||
export const GalleryImageDeleteIconButton = memo(({ imageDTO }: Props) => {
|
||||
export const GalleryItemDeleteIconButton = memo(({ itemDTO }: Props) => {
|
||||
const shift = useShiftModifier();
|
||||
const { t } = useTranslation();
|
||||
const deleteImageModal = useDeleteImageModalApi();
|
||||
const [deleteVideos] = useDeleteVideosMutation();
|
||||
|
||||
const onClick = useCallback(
|
||||
(e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
if (!imageDTO) {
|
||||
if (!itemDTO) {
|
||||
return;
|
||||
}
|
||||
deleteImageModal.delete([imageDTO.image_name]);
|
||||
if (isImageDTO(itemDTO)) {
|
||||
deleteImageModal.delete([itemDTO.image_name]);
|
||||
} else {
|
||||
// TODO: Add confirm on delete and video usage functionality
|
||||
deleteVideos({ video_ids: [itemDTO.video_id] });
|
||||
}
|
||||
},
|
||||
[deleteImageModal, imageDTO]
|
||||
[deleteImageModal, deleteVideos, itemDTO]
|
||||
);
|
||||
|
||||
if (!shift) {
|
||||
@@ -43,4 +50,4 @@ export const GalleryImageDeleteIconButton = memo(({ imageDTO }: Props) => {
|
||||
);
|
||||
});
|
||||
|
||||
GalleryImageDeleteIconButton.displayName = 'GalleryImageDeleteIconButton';
|
||||
GalleryItemDeleteIconButton.displayName = 'GalleryItemDeleteIconButton';
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { GalleryItemDeleteIconButton } from 'features/gallery/components/ImageGrid/GalleryItemDeleteIconButton';
|
||||
import { GalleryItemOpenInViewerIconButton } from 'features/gallery/components/ImageGrid/GalleryItemOpenInViewerIconButton';
|
||||
import { GalleryItemSizeBadge } from 'features/gallery/components/ImageGrid/GalleryItemSizeBadge';
|
||||
import { GalleryItemStarIconButton } from 'features/gallery/components/ImageGrid/GalleryItemStarIconButton';
|
||||
import { selectAlwaysShouldImageSizeBadge } from 'features/gallery/store/gallerySelectors';
|
||||
import { memo } from 'react';
|
||||
import type { ImageDTO, VideoDTO } from 'services/api/types';
|
||||
|
||||
type Props = {
|
||||
itemDTO: ImageDTO | VideoDTO;
|
||||
isHovered: boolean;
|
||||
};
|
||||
|
||||
export const GalleryItemHoverIcons = memo(({ itemDTO, isHovered }: Props) => {
|
||||
const alwaysShowImageSizeBadge = useAppSelector(selectAlwaysShouldImageSizeBadge);
|
||||
|
||||
return (
|
||||
<>
|
||||
{(isHovered || alwaysShowImageSizeBadge) && <GalleryItemSizeBadge itemDTO={itemDTO} />}
|
||||
{(isHovered || itemDTO.starred) && <GalleryItemStarIconButton itemDTO={itemDTO} />}
|
||||
{isHovered && <GalleryItemDeleteIconButton itemDTO={itemDTO} />}
|
||||
{isHovered && <GalleryItemOpenInViewerIconButton itemDTO={itemDTO} />}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
GalleryItemHoverIcons.displayName = 'GalleryItemHoverIcons';
|
||||
@@ -1,26 +1,31 @@
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { DndImageIcon } from 'features/dnd/DndImageIcon';
|
||||
import { imageSelected, imageToCompareChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { imageToCompareChanged, itemSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
import { VIEWER_PANEL_ID } from 'features/ui/layouts/shared';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowsOutBold } from 'react-icons/pi';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import { type ImageDTO, isImageDTO, type VideoDTO } from 'services/api/types';
|
||||
|
||||
type Props = {
|
||||
imageDTO: ImageDTO;
|
||||
itemDTO: ImageDTO | VideoDTO;
|
||||
};
|
||||
|
||||
export const GalleryImageOpenInViewerIconButton = memo(({ imageDTO }: Props) => {
|
||||
export const GalleryItemOpenInViewerIconButton = memo(({ itemDTO }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(imageToCompareChanged(null));
|
||||
dispatch(imageSelected(imageDTO.image_name));
|
||||
if (isImageDTO(itemDTO)) {
|
||||
dispatch(imageToCompareChanged(null));
|
||||
dispatch(itemSelected({ type: 'image', id: itemDTO.image_name }));
|
||||
} else {
|
||||
// dispatch(videoToCompareChanged(null));
|
||||
// dispatch(videoSelected(itemDTO.video_id));
|
||||
}
|
||||
navigationApi.focusPanelInActiveTab(VIEWER_PANEL_ID);
|
||||
}, [dispatch, imageDTO]);
|
||||
}, [dispatch, itemDTO]);
|
||||
|
||||
return (
|
||||
<DndImageIcon
|
||||
@@ -34,4 +39,4 @@ export const GalleryImageOpenInViewerIconButton = memo(({ imageDTO }: Props) =>
|
||||
);
|
||||
});
|
||||
|
||||
GalleryImageOpenInViewerIconButton.displayName = 'GalleryImageOpenInViewerIconButton';
|
||||
GalleryItemOpenInViewerIconButton.displayName = 'GalleryItemOpenInViewerIconButton';
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Text } from '@invoke-ai/ui-library';
|
||||
import { memo } from 'react';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import type { ImageDTO, VideoDTO } from 'services/api/types';
|
||||
|
||||
type Props = {
|
||||
imageDTO: ImageDTO;
|
||||
itemDTO: ImageDTO | VideoDTO;
|
||||
};
|
||||
|
||||
export const GalleryImageSizeBadge = memo(({ imageDTO }: Props) => {
|
||||
export const GalleryItemSizeBadge = memo(({ itemDTO }: Props) => {
|
||||
return (
|
||||
<Text
|
||||
className="gallery-image-size-badge"
|
||||
@@ -22,8 +22,8 @@ export const GalleryImageSizeBadge = memo(({ imageDTO }: Props) => {
|
||||
lineHeight={1.25}
|
||||
borderTopEndRadius="base"
|
||||
pointerEvents="none"
|
||||
>{`${imageDTO.width}x${imageDTO.height}`}</Text>
|
||||
>{`${itemDTO.width}x${itemDTO.height}`}</Text>
|
||||
);
|
||||
});
|
||||
|
||||
GalleryImageSizeBadge.displayName = 'GalleryImageSizeBadge';
|
||||
GalleryItemSizeBadge.displayName = 'GalleryItemSizeBadge';
|
||||
@@ -0,0 +1,62 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $customStarUI } from 'app/store/nanostores/customStarUI';
|
||||
import { DndImageIcon } from 'features/dnd/DndImageIcon';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { PiStarBold, PiStarFill } from 'react-icons/pi';
|
||||
import { useStarImagesMutation, useUnstarImagesMutation } from 'services/api/endpoints/images';
|
||||
import { useStarVideosMutation, useUnstarVideosMutation } from 'services/api/endpoints/videos';
|
||||
import { type ImageDTO, isImageDTO, type VideoDTO } from 'services/api/types';
|
||||
|
||||
type Props = {
|
||||
itemDTO: ImageDTO | VideoDTO;
|
||||
};
|
||||
|
||||
export const GalleryItemStarIconButton = memo(({ itemDTO }: Props) => {
|
||||
const customStarUi = useStore($customStarUI);
|
||||
const [starImages] = useStarImagesMutation();
|
||||
const [unstarImages] = useUnstarImagesMutation();
|
||||
const [starVideos] = useStarVideosMutation();
|
||||
const [unstarVideos] = useUnstarVideosMutation();
|
||||
|
||||
const toggleStarredState = useCallback(() => {
|
||||
if (itemDTO.starred) {
|
||||
if (isImageDTO(itemDTO)) {
|
||||
unstarImages({ image_names: [itemDTO.image_name] });
|
||||
} else {
|
||||
unstarVideos({ video_ids: [itemDTO.video_id] });
|
||||
}
|
||||
} else {
|
||||
if (isImageDTO(itemDTO)) {
|
||||
starImages({ image_names: [itemDTO.image_name] });
|
||||
} else {
|
||||
starVideos({ video_ids: [itemDTO.video_id] });
|
||||
}
|
||||
}
|
||||
}, [starImages, unstarImages, starVideos, unstarVideos, itemDTO]);
|
||||
|
||||
if (customStarUi) {
|
||||
return (
|
||||
<DndImageIcon
|
||||
onClick={toggleStarredState}
|
||||
icon={itemDTO.starred ? customStarUi.on.icon : customStarUi.off.icon}
|
||||
tooltip={itemDTO.starred ? customStarUi.on.text : customStarUi.off.text}
|
||||
position="absolute"
|
||||
top={2}
|
||||
insetInlineEnd={2}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DndImageIcon
|
||||
onClick={toggleStarredState}
|
||||
icon={itemDTO.starred ? <PiStarFill /> : <PiStarBold />}
|
||||
tooltip={itemDTO.starred ? 'Unstar' : 'Star'}
|
||||
position="absolute"
|
||||
top={2}
|
||||
insetInlineEnd={2}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
GalleryItemStarIconButton.displayName = 'GalleryItemStarIconButton';
|
||||
@@ -2,7 +2,7 @@ import { Tag, TagCloseButton, TagLabel } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { useIsRegionFocused } from 'common/hooks/focus';
|
||||
import { useGalleryImageNames } from 'features/gallery/components/use-gallery-image-names';
|
||||
import { selectFirstSelectedImage, selectSelectionCount } from 'features/gallery/store/gallerySelectors';
|
||||
import { selectFirstSelectedItem, selectSelectionCount } from 'features/gallery/store/gallerySelectors';
|
||||
import { selectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { memo, useCallback } from 'react';
|
||||
@@ -15,7 +15,8 @@ export const GallerySelectionCountTag = memo(() => {
|
||||
const isGalleryFocused = useIsRegionFocused('gallery');
|
||||
|
||||
const onSelectPage = useCallback(() => {
|
||||
dispatch(selectionChanged([...imageNames]));
|
||||
const selection = imageNames.map((name) => ({ type: 'image' as const, id: name }));
|
||||
dispatch(selectionChanged(selection));
|
||||
}, [dispatch, imageNames]);
|
||||
|
||||
useRegisteredHotkeys({
|
||||
@@ -39,13 +40,13 @@ const GallerySelectionCountTagContent = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const isGalleryFocused = useIsRegionFocused('gallery');
|
||||
const firstImage = useAppSelector(selectFirstSelectedImage);
|
||||
const firstItem = useAppSelector(selectFirstSelectedItem);
|
||||
const selectionCount = useAppSelector(selectSelectionCount);
|
||||
const onClearSelection = useCallback(() => {
|
||||
if (firstImage) {
|
||||
dispatch(selectionChanged([firstImage]));
|
||||
if (firstItem) {
|
||||
dispatch(selectionChanged([firstItem]));
|
||||
}
|
||||
}, [dispatch, firstImage]);
|
||||
}, [dispatch, firstItem]);
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'clearSelection',
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
||||
import { draggable, monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { Flex, Image } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import type { AppDispatch, AppGetState } from 'app/store/store';
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { uniq } from 'es-toolkit';
|
||||
import { multipleVideoDndSource, singleVideoDndSource } from 'features/dnd/dnd';
|
||||
import type { DndDragPreviewMultipleVideoState } from 'features/dnd/DndDragPreviewMultipleVideo';
|
||||
import { createMultipleVideoDragPreview, setMultipleVideoDragPreview } from 'features/dnd/DndDragPreviewMultipleVideo';
|
||||
import type { DndDragPreviewSingleVideoState } from 'features/dnd/DndDragPreviewSingleVideo';
|
||||
import { createSingleVideoDragPreview, setSingleVideoDragPreview } from 'features/dnd/DndDragPreviewSingleVideo';
|
||||
import { firefoxDndFix } from 'features/dnd/util';
|
||||
import { useVideoContextMenu } from 'features/gallery/components/ContextMenu/VideoContextMenu';
|
||||
import {
|
||||
selectGetVideoIdsQueryArgs,
|
||||
selectSelectedBoardId,
|
||||
selectSelection,
|
||||
} from 'features/gallery/store/gallerySelectors';
|
||||
import { imageToCompareChanged, selectGallerySlice, selectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
import { VIEWER_PANEL_ID } from 'features/ui/layouts/shared';
|
||||
import type { MouseEvent, MouseEventHandler } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { videosApi } from 'services/api/endpoints/videos';
|
||||
import type { VideoDTO } from 'services/api/types';
|
||||
|
||||
import { galleryItemContainerSX } from './galleryItemContainerSX';
|
||||
import { GalleryItemHoverIcons } from './GalleryItemHoverIcons';
|
||||
import { GalleryVideoPlaceholder } from './GalleryVideoPlaceholder';
|
||||
|
||||
interface Props {
|
||||
videoDTO: VideoDTO;
|
||||
}
|
||||
|
||||
const buildOnClick =
|
||||
(videoId: string, dispatch: AppDispatch, getState: AppGetState) => (e: MouseEvent<HTMLDivElement>) => {
|
||||
const { shiftKey, ctrlKey, metaKey, altKey } = e;
|
||||
const state = getState();
|
||||
const queryArgs = selectGetVideoIdsQueryArgs(state);
|
||||
const videoIds = videosApi.endpoints.getVideoIds.select(queryArgs)(state).data?.video_ids ?? [];
|
||||
|
||||
// If we don't have the video ids cached, we can't perform selection operations
|
||||
// This can happen if the user clicks on a video before the ids are loaded
|
||||
if (videoIds.length === 0) {
|
||||
// For basic click without modifiers, we can still set selection
|
||||
if (!shiftKey && !ctrlKey && !metaKey && !altKey) {
|
||||
dispatch(selectionChanged([{ type: 'video', id: videoId }]));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const selection = state.gallery.selection;
|
||||
|
||||
if (altKey) {
|
||||
if (state.gallery.imageToCompare === videoId) {
|
||||
dispatch(imageToCompareChanged(null));
|
||||
} else {
|
||||
dispatch(imageToCompareChanged(videoId));
|
||||
}
|
||||
} else if (shiftKey) {
|
||||
const rangeEndVideoId = videoId;
|
||||
const lastSelectedVideo = selection.at(-1)?.id;
|
||||
const lastClickedIndex = videoIds.findIndex((id) => id === lastSelectedVideo);
|
||||
const currentClickedIndex = videoIds.findIndex((id) => id === rangeEndVideoId);
|
||||
if (lastClickedIndex > -1 && currentClickedIndex > -1) {
|
||||
// We have a valid range!
|
||||
const start = Math.min(lastClickedIndex, currentClickedIndex);
|
||||
const end = Math.max(lastClickedIndex, currentClickedIndex);
|
||||
const videosToSelect = videoIds.slice(start, end + 1);
|
||||
dispatch(selectionChanged(uniq(selection.concat(videosToSelect.map((id) => ({ type: 'video', id }))))));
|
||||
}
|
||||
} else if (ctrlKey || metaKey) {
|
||||
if (selection.some((n) => n.id === videoId) && selection.length > 1) {
|
||||
dispatch(selectionChanged(uniq(selection.filter((n) => n.id !== videoId))));
|
||||
} else {
|
||||
dispatch(selectionChanged(uniq(selection.concat({ type: 'video', id: videoId }))));
|
||||
}
|
||||
} else {
|
||||
dispatch(selectionChanged([{ type: 'video', id: videoId }]));
|
||||
}
|
||||
};
|
||||
|
||||
export const GalleryVideo = memo(({ videoDTO }: Props) => {
|
||||
const store = useAppStore();
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [dragPreviewState, setDragPreviewState] = useState<
|
||||
DndDragPreviewSingleVideoState | DndDragPreviewMultipleVideoState | null
|
||||
>(null);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const selectIsSelected = useMemo(
|
||||
() => createSelector(selectGallerySlice, (gallery) => gallery.selection.some((s) => s.id === videoDTO.video_id)),
|
||||
[videoDTO.video_id]
|
||||
);
|
||||
const isSelected = useAppSelector(selectIsSelected);
|
||||
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
return combine(
|
||||
firefoxDndFix(element),
|
||||
draggable({
|
||||
element,
|
||||
getInitialData: () => {
|
||||
const selection = selectSelection(store.getState());
|
||||
const boardId = selectSelectedBoardId(store.getState());
|
||||
|
||||
// When we have multiple images selected, and the dragged image is part of the selection, initiate a
|
||||
// multi-image drag.
|
||||
if (selection.length > 1 && selection.some((s) => s.id === videoDTO.video_id)) {
|
||||
return multipleVideoDndSource.getData({
|
||||
video_ids: selection.map((s) => s.id),
|
||||
board_id: boardId,
|
||||
});
|
||||
} // Otherwise, initiate a single-image drag
|
||||
|
||||
return singleVideoDndSource.getData({ videoDTO }, videoDTO.video_id);
|
||||
},
|
||||
// This is a "local" drag start event, meaning that it is only called when this specific image is dragged.
|
||||
onDragStart: ({ source }) => {
|
||||
// When we start dragging a single image, set the dragging state to true. This is only called when this
|
||||
// specific image is dragged.
|
||||
if (singleVideoDndSource.typeGuard(source.data)) {
|
||||
setIsDragging(true);
|
||||
return;
|
||||
}
|
||||
},
|
||||
onGenerateDragPreview: (args) => {
|
||||
if (multipleVideoDndSource.typeGuard(args.source.data)) {
|
||||
setMultipleVideoDragPreview({
|
||||
multipleVideoDndData: args.source.data,
|
||||
onGenerateDragPreviewArgs: args,
|
||||
setDragPreviewState,
|
||||
});
|
||||
} else if (singleVideoDndSource.typeGuard(args.source.data)) {
|
||||
setSingleVideoDragPreview({
|
||||
singleVideoDndData: args.source.data,
|
||||
onGenerateDragPreviewArgs: args,
|
||||
setDragPreviewState,
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
monitorForElements({
|
||||
// This is a "global" drag start event, meaning that it is called for all drag events.
|
||||
onDragStart: ({ source }) => {
|
||||
// When we start dragging multiple images, set the dragging state to true if the dragged image is part of the
|
||||
// selection. This is called for all drag events.
|
||||
if (
|
||||
multipleVideoDndSource.typeGuard(source.data) &&
|
||||
source.data.payload.video_ids.includes(videoDTO.video_id)
|
||||
) {
|
||||
setIsDragging(true);
|
||||
}
|
||||
},
|
||||
onDrop: () => {
|
||||
// Always set the dragging state to false when a drop event occurs.
|
||||
setIsDragging(false);
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [videoDTO, store]);
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const onMouseOver = useCallback(() => {
|
||||
setIsHovered(true);
|
||||
}, []);
|
||||
|
||||
const onMouseOut = useCallback(() => {
|
||||
setIsHovered(false);
|
||||
}, []);
|
||||
|
||||
const onClick = useMemo(() => buildOnClick(videoDTO.video_id, store.dispatch, store.getState), [videoDTO, store]);
|
||||
|
||||
const onDoubleClick = useCallback<MouseEventHandler<HTMLDivElement>>(() => {
|
||||
store.dispatch(imageToCompareChanged(null));
|
||||
navigationApi.focusPanelInActiveTab(VIEWER_PANEL_ID);
|
||||
}, [store]);
|
||||
|
||||
useVideoContextMenu(videoDTO, ref);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
ref={ref}
|
||||
sx={galleryItemContainerSX}
|
||||
data-is-dragging={isDragging}
|
||||
data-item-id={videoDTO.video_id}
|
||||
role="button"
|
||||
onMouseOver={onMouseOver}
|
||||
onMouseOut={onMouseOut}
|
||||
onClick={onClick}
|
||||
onDoubleClick={onDoubleClick}
|
||||
data-selected={isSelected}
|
||||
data-selected-for-compare={false}
|
||||
>
|
||||
<Image
|
||||
pointerEvents="none"
|
||||
src={videoDTO.thumbnail_url}
|
||||
w={videoDTO.width}
|
||||
fallback={<GalleryVideoPlaceholder />}
|
||||
objectFit="contain"
|
||||
maxW="full"
|
||||
maxH="full"
|
||||
borderRadius="base"
|
||||
/>
|
||||
<GalleryItemHoverIcons itemDTO={videoDTO} isHovered={isHovered} />
|
||||
</Flex>
|
||||
{dragPreviewState?.type === 'multiple-video' ? createMultipleVideoDragPreview(dragPreviewState) : null}
|
||||
{dragPreviewState?.type === 'single-video' ? createSingleVideoDragPreview(dragPreviewState) : null}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
GalleryVideo.displayName = 'GalleryVideo';
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Flex, type FlexProps, Icon } from '@invoke-ai/ui-library';
|
||||
import { memo } from 'react';
|
||||
import { PiVideoBold } from 'react-icons/pi';
|
||||
|
||||
export const GalleryVideoPlaceholder = memo((props: FlexProps) => (
|
||||
<Flex w="full" h="full" bg="base.850" borderRadius="base" alignItems="center" justifyContent="center" {...props}>
|
||||
<Icon as={PiVideoBold} boxSize={16} color="base.800" />
|
||||
</Flex>
|
||||
));
|
||||
|
||||
GalleryVideoPlaceholder.displayName = 'GalleryVideoPlaceholder';
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
|
||||
export const galleryItemContainerSX = {
|
||||
containerType: 'inline-size',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
'.gallery-image-size-badge': {
|
||||
'@container (max-width: 80px)': {
|
||||
'&': { display: 'none' },
|
||||
},
|
||||
},
|
||||
'&[data-is-dragging=true]': {
|
||||
opacity: 0.3,
|
||||
},
|
||||
userSelect: 'none',
|
||||
webkitUserSelect: 'none',
|
||||
position: 'relative',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
aspectRatio: '1/1',
|
||||
'::before': {
|
||||
content: '""',
|
||||
display: 'inline-block',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
pointerEvents: 'none',
|
||||
borderRadius: 'base',
|
||||
},
|
||||
'&[data-selected=true]::before': {
|
||||
boxShadow:
|
||||
'inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-500), inset 0px 0px 0px 4px var(--invoke-colors-invokeBlue-800)',
|
||||
},
|
||||
'&[data-selected-for-compare=true]::before': {
|
||||
boxShadow:
|
||||
'inset 0px 0px 0px 3px var(--invoke-colors-invokeGreen-300), inset 0px 0px 0px 4px var(--invoke-colors-invokeGreen-800)',
|
||||
},
|
||||
'&:hover::before': {
|
||||
boxShadow:
|
||||
'inset 0px 0px 0px 1px var(--invoke-colors-invokeBlue-300), inset 0px 0px 0px 2px var(--invoke-colors-invokeBlue-800)',
|
||||
},
|
||||
'&:hover[data-selected=true]::before': {
|
||||
boxShadow:
|
||||
'inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-400), inset 0px 0px 0px 4px var(--invoke-colors-invokeBlue-800)',
|
||||
},
|
||||
'&:hover[data-selected-for-compare=true]::before': {
|
||||
boxShadow:
|
||||
'inset 0px 0px 0px 3px var(--invoke-colors-invokeGreen-200), inset 0px 0px 0px 4px var(--invoke-colors-invokeGreen-800)',
|
||||
},
|
||||
} satisfies SystemStyleObject;
|
||||
@@ -9,10 +9,11 @@ import type {
|
||||
UnrecallableMetadataHandler,
|
||||
} from 'features/metadata/parsing';
|
||||
import {
|
||||
MetadataHandlers,
|
||||
ImageMetadataHandlers,
|
||||
useCollectionMetadataDatum,
|
||||
useSingleMetadataDatum,
|
||||
useUnrecallableMetadataDatum,
|
||||
VideoMetadataHandlers,
|
||||
} from 'features/metadata/parsing';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { PiArrowBendUpLeftBold } from 'react-icons/pi';
|
||||
@@ -21,7 +22,7 @@ type Props = {
|
||||
metadata?: unknown;
|
||||
};
|
||||
|
||||
const ImageMetadataActions = (props: Props) => {
|
||||
export const ImageMetadataActions = memo((props: Props) => {
|
||||
const { metadata } = props;
|
||||
|
||||
if (!metadata || Object.keys(metadata).length === 0) {
|
||||
@@ -30,38 +31,60 @@ const ImageMetadataActions = (props: Props) => {
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" ps={8}>
|
||||
<UnrecallableMetadataDatum metadata={metadata} handler={MetadataHandlers.GenerationMode} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.PositivePrompt} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.NegativePrompt} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.MainModel} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.VAEModel} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.Width} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.Height} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.Seed} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.Steps} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.Scheduler} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.CLIPSkip} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.CFGScale} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.CFGRescaleMultiplier} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.Guidance} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.DenoisingStrength} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.SeamlessX} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.SeamlessY} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.RefinerModel} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.RefinerCFGScale} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.RefinerPositiveAestheticScore} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.RefinerNegativeAestheticScore} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.RefinerScheduler} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.RefinerDenoisingStart} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.RefinerSteps} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.CanvasLayers} />
|
||||
<CollectionMetadataDatum metadata={metadata} handler={MetadataHandlers.RefImages} />
|
||||
<CollectionMetadataDatum metadata={metadata} handler={MetadataHandlers.LoRAs} />
|
||||
<UnrecallableMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.GenerationMode} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.PositivePrompt} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.NegativePrompt} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.MainModel} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.VAEModel} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.Width} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.Height} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.Seed} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.Steps} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.Scheduler} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.CLIPSkip} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.CFGScale} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.CFGRescaleMultiplier} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.Guidance} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.DenoisingStrength} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.SeamlessX} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.SeamlessY} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.RefinerModel} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.RefinerCFGScale} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.RefinerPositiveAestheticScore} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.RefinerNegativeAestheticScore} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.RefinerScheduler} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.RefinerDenoisingStart} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.RefinerSteps} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.CanvasLayers} />
|
||||
<CollectionMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.RefImages} />
|
||||
<CollectionMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.LoRAs} />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default memo(ImageMetadataActions);
|
||||
ImageMetadataActions.displayName = 'ImageMetadataActions';
|
||||
|
||||
export const VideoMetadataActions = memo((props: Props) => {
|
||||
const { metadata } = props;
|
||||
|
||||
if (!metadata || Object.keys(metadata).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" ps={8}>
|
||||
<UnrecallableMetadataDatum metadata={metadata} handler={VideoMetadataHandlers.GenerationMode} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={VideoMetadataHandlers.PositivePrompt} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={VideoMetadataHandlers.Seed} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={VideoMetadataHandlers.VideoAspectRatio} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={VideoMetadataHandlers.VideoDuration} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={VideoMetadataHandlers.VideoResolution} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={VideoMetadataHandlers.VideoModel} />
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
VideoMetadataActions.displayName = 'VideoMetadataActions';
|
||||
|
||||
export const UnrecallableMetadataDatum = typedMemo(
|
||||
<T,>({ metadata, handler }: { metadata: unknown; handler: UnrecallableMetadataHandler<T> }) => {
|
||||
|
||||
@@ -2,14 +2,14 @@ import { ExternalLink, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@in
|
||||
import { IAINoContentFallback, IAINoContentFallbackWithSpinner } from 'common/components/IAIImageFallback';
|
||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||
import ImageMetadataGraphTabContent from 'features/gallery/components/ImageMetadataViewer/ImageMetadataGraphTabContent';
|
||||
import { MetadataHandlers } from 'features/metadata/parsing';
|
||||
import { ImageMetadataHandlers } from 'features/metadata/parsing';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
import DataViewer from './DataViewer';
|
||||
import ImageMetadataActions, { UnrecallableMetadataDatum } from './ImageMetadataActions';
|
||||
import { ImageMetadataActions, UnrecallableMetadataDatum } from './ImageMetadataActions';
|
||||
import ImageMetadataWorkflowTabContent from './ImageMetadataWorkflowTabContent';
|
||||
|
||||
type ImageMetadataViewerProps = {
|
||||
@@ -39,7 +39,7 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
|
||||
overflow="hidden"
|
||||
>
|
||||
<ExternalLink href={image.image_url} label={image.image_name} />
|
||||
<UnrecallableMetadataDatum metadata={metadata} handler={MetadataHandlers.CreatedBy} />
|
||||
<UnrecallableMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.CreatedBy} />
|
||||
|
||||
<Tabs variant="line" isLazy={true} display="flex" flexDir="column" w="full" h="full">
|
||||
<TabList>
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { ExternalLink, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
|
||||
import { IAINoContentFallback, IAINoContentFallbackWithSpinner } from 'common/components/IAIImageFallback';
|
||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||
import { ImageMetadataHandlers } from 'features/metadata/parsing';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDebouncedVideoMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
||||
import type { VideoDTO } from 'services/api/types';
|
||||
|
||||
import DataViewer from './DataViewer';
|
||||
import { UnrecallableMetadataDatum, VideoMetadataActions } from './ImageMetadataActions';
|
||||
|
||||
type VideoMetadataViewerProps = {
|
||||
video: VideoDTO;
|
||||
};
|
||||
|
||||
const VideoMetadataViewer = ({ video }: VideoMetadataViewerProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { metadata, isLoading } = useDebouncedVideoMetadata(video.video_id);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
layerStyle="first"
|
||||
padding={4}
|
||||
gap={1}
|
||||
flexDirection="column"
|
||||
width="full"
|
||||
height="full"
|
||||
borderRadius="base"
|
||||
position="absolute"
|
||||
overflow="hidden"
|
||||
>
|
||||
<ExternalLink href={video.video_url} label={video.video_id} />
|
||||
<UnrecallableMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.CreatedBy} />
|
||||
|
||||
<Tabs variant="line" isLazy={true} display="flex" flexDir="column" w="full" h="full">
|
||||
<TabList>
|
||||
<Tab>{t('metadata.recallParameters')}</Tab>
|
||||
<Tab>{t('metadata.metadata')}</Tab>
|
||||
<Tab>{t('metadata.videoDetails')}</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
{isLoading && <IAINoContentFallbackWithSpinner label="Loading metadata..." />}
|
||||
{metadata && !isLoading && (
|
||||
<ScrollableContent>
|
||||
<VideoMetadataActions metadata={metadata} />
|
||||
</ScrollableContent>
|
||||
)}
|
||||
{!metadata && !isLoading && <IAINoContentFallback label={t('metadata.noRecallParameters')} />}
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
{metadata ? (
|
||||
<DataViewer fileName={`${video.video_id}_metadata`} data={metadata} label={t('metadata.metadata')} />
|
||||
) : (
|
||||
<IAINoContentFallback label={t('metadata.noMetaData')} />
|
||||
)}
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
{video ? (
|
||||
<DataViewer fileName={`${video.video_id}_details`} data={video} label={t('metadata.videoDetails')} />
|
||||
) : (
|
||||
<IAINoContentFallback label={t('metadata.noVideoDetails')} />
|
||||
)}
|
||||
</TabPanel>
|
||||
{/* <TabPanel>
|
||||
<ImageMetadataWorkflowTabContent image={image} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<ImageMetadataGraphTabContent image={image} />
|
||||
</TabPanel> */}
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(VideoMetadataViewer);
|
||||
@@ -1,16 +1,17 @@
|
||||
import { Button, Divider, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton';
|
||||
import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems';
|
||||
import SingleSelectionMenuItems from 'features/gallery/components/ContextMenu/SingleSelectionMenuItems';
|
||||
import { useDeleteImage } from 'features/gallery/hooks/useDeleteImage';
|
||||
import { useEditImage } from 'features/gallery/hooks/useEditImage';
|
||||
import { useLoadWorkflow } from 'features/gallery/hooks/useLoadWorkflow';
|
||||
import { useRecallAll } from 'features/gallery/hooks/useRecallAll';
|
||||
import { useRecallAll } from 'features/gallery/hooks/useRecallAllImageMetadata';
|
||||
import { useRecallDimensions } from 'features/gallery/hooks/useRecallDimensions';
|
||||
import { useRecallPrompts } from 'features/gallery/hooks/useRecallPrompts';
|
||||
import { useRecallRemix } from 'features/gallery/hooks/useRecallRemix';
|
||||
import { useRecallSeed } from 'features/gallery/hooks/useRecallSeed';
|
||||
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { IMAGE_CATEGORIES } from 'features/gallery/store/types';
|
||||
import { PostProcessingPopover } from 'features/parameters/components/PostProcessing/PostProcessingPopover';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
@@ -47,7 +48,15 @@ export const CurrentImageButtons = memo(({ imageDTO }: { imageDTO: ImageDTO }) =
|
||||
navigationApi.expandRightPanel();
|
||||
galleryPanel.expand();
|
||||
flushSync(() => {
|
||||
dispatch(boardIdSelected({ boardId: imageDTO.board_id ?? 'none', selectedImageName: imageDTO.image_name }));
|
||||
dispatch(
|
||||
boardIdSelected({
|
||||
boardId: imageDTO.board_id ?? 'none',
|
||||
select: {
|
||||
selection: [{ type: 'image', id: imageDTO.image_name }],
|
||||
galleryView: IMAGE_CATEGORIES.includes(imageDTO.image_category) ? 'images' : 'assets',
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
}, [dispatch, galleryPanel, imageDTO]);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user