stubbing out change board functionality

This commit is contained in:
Mary Hipp
2025-08-19 16:28:05 -04:00
committed by Mary Hipp Rogers
parent a918198d4f
commit 67042e6dec
8 changed files with 254 additions and 15 deletions

View File

@@ -0,0 +1,38 @@
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")

View File

@@ -20,6 +20,7 @@ from invokeai.app.api.routers import (
board_images,
boards,
client_state,
board_videos,
download_queue,
images,
model_manager,
@@ -129,6 +130,7 @@ 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")

View File

@@ -5,14 +5,15 @@ import { imageDownloaded } from 'features/gallery/store/actions';
import { toast } from 'features/toast/toast';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { ImageDTO, VideoDTO } from 'services/api/types';
export const useDownloadImage = () => {
export const useDownloadItem = (itemDTO: ImageDTO | VideoDTO) => {
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 +22,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 +31,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 +48,5 @@ export const useDownloadImage = () => {
[t, dispatch, authToken]
);
return { downloadImage };
return { downloadItem };
};

View File

@@ -4,11 +4,13 @@ import { useItemDTOContext } from 'features/gallery/contexts/ItemDTOContext';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiTrashSimpleBold } from 'react-icons/pi';
import { useDeleteVideosMutation } from 'services/api/endpoints/videos';
import { isImageDTO } from 'services/api/types';
export const ContextMenuItemDelete = memo(() => {
const { t } = useTranslation();
const deleteImageModal = useDeleteImageModalApi();
const [deleteVideos] = useDeleteVideosMutation();
const itemDTO = useItemDTOContext();
const onClick = useCallback(async () => {
@@ -16,12 +18,13 @@ export const ContextMenuItemDelete = memo(() => {
if (isImageDTO(itemDTO)) {
await deleteImageModal.delete([itemDTO.image_name]);
} else {
// await deleteVideoModal.delete([itemDTO.video_id]);
// TODO: Add confirm on delete and video usage functionality
await deleteVideos({ video_ids: [itemDTO.video_id] });
}
} catch {
// noop;
}
}, [deleteImageModal, itemDTO]);
}, [deleteImageModal, deleteVideos, itemDTO]);
return (
<IconMenuItem

View File

@@ -1,5 +1,5 @@
import { IconMenuItem } from 'common/components/IconMenuItem';
import { useDownloadImage } from 'common/hooks/useDownloadImage';
import { useDownloadItem } from 'common/hooks/useDownloadImage';
import { useItemDTOContext } from 'features/gallery/contexts/ItemDTOContext';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -9,15 +9,15 @@ import { isImageDTO } from 'services/api/types';
export const ContextMenuItemDownload = memo(() => {
const { t } = useTranslation();
const itemDTO = useItemDTOContext();
const { downloadImage } = useDownloadImage();
const { downloadItem } = useDownloadItem(itemDTO);
const onClick = useCallback(() => {
if (isImageDTO(itemDTO)) {
downloadImage(itemDTO.image_url, itemDTO.image_name);
downloadItem(itemDTO.image_url, itemDTO.image_name);
} else {
// downloadVideo(itemDTO.video_url, itemDTO.video_id);
downloadItem(itemDTO.video_url, itemDTO.video_id);
}
}, [downloadImage, itemDTO]);
}, [downloadItem, itemDTO]);
return (
<IconMenuItem

View File

@@ -16,7 +16,7 @@ export const ContextMenuItemOpenInNewTab = memo(() => {
window.open(itemDTO.image_url, '_blank');
dispatch(imageOpenedInNewTab());
} else {
// TODO: Implement video open in new tab
window.open(itemDTO.video_url, '_blank');
}
}, [itemDTO, dispatch]);

View File

@@ -8,7 +8,7 @@ import stableHash from 'stable-hash';
import type { Param0 } from 'tsafe';
import { api, buildV1Url, LIST_TAG } from '..';
import { getTagsToInvalidateForBoardAffectingMutation, getTagsToInvalidateForVideoMutation } from '../util/tagInvalidation';
import { getTagsToInvalidateForBoardAffectingMutation, getTagsToInvalidateForImageMutation, getTagsToInvalidateForVideoMutation } from '../util/tagInvalidation';
/**
* Builds an endpoint URL for the videos router
@@ -19,6 +19,8 @@ import { getTagsToInvalidateForBoardAffectingMutation, getTagsToInvalidateForVid
const buildVideosUrl = (path: string = '', query?: Parameters<typeof buildV1Url>[1]) =>
buildV1Url(`videos/${path}`, query);
const buildBoardVideosUrl = (path: string = '') => buildV1Url(`board_videos/${path}`);
export const videosApi = api.injectEndpoints({
endpoints: (build) => ({
/**
@@ -150,6 +152,44 @@ export const videosApi = api.injectEndpoints({
];
},
}),
addVideosToBoard: build.mutation<
paths['/api/v1/board_videos/batch']['post']['responses']['201']['content']['application/json'],
paths['/api/v1/board_videos/batch']['post']['requestBody']['content']['application/json']
>({
query: (body) => ({
url: buildBoardVideosUrl('batch'),
method: 'POST',
body,
}),
invalidatesTags: (result) => {
if (!result) {
return [];
}
return [
...getTagsToInvalidateForVideoMutation(result.added_videos),
...getTagsToInvalidateForBoardAffectingMutation(result.affected_boards),
];
},
}),
removeVideosFromBoard: build.mutation<
paths['/api/v1/board_videos/batch/delete']['post']['responses']['201']['content']['application/json'],
paths['/api/v1/board_videos/batch/delete']['post']['requestBody']['content']['application/json']
>({
query: (body) => ({
url: buildBoardVideosUrl('batch/delete'),
method: 'POST',
body,
}),
invalidatesTags: (result) => {
if (!result) {
return [];
}
return [
...getTagsToInvalidateForVideoMutation(result.removed_videos),
...getTagsToInvalidateForBoardAffectingMutation(result.affected_boards),
];
},
}),
}),
});
@@ -160,6 +200,8 @@ export const {
useStarVideosMutation,
useUnstarVideosMutation,
useDeleteVideosMutation,
useAddVideosToBoardMutation,
useRemoveVideosFromBoardMutation,
} = videosApi;

View File

@@ -1083,6 +1083,46 @@ export type paths = {
patch?: never;
trace?: never;
};
"/api/v1/board_videos/batch": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Add Videos To Board
* @description Adds a list of videos to a board
*/
post: operations["add_videos_to_board"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/board_videos/batch/delete": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Remove Videos From Board
* @description Removes a list of videos from their board, if they had one
*/
post: operations["remove_videos_from_board"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/model_relationships/i/{model_key}": {
parameters: {
query?: never;
@@ -2087,6 +2127,19 @@ export type components = {
*/
type: "add";
};
/** AddVideosToBoardResult */
AddVideosToBoardResult: {
/**
* Affected Boards
* @description The ids of boards affected by the delete operation
*/
affected_boards: string[];
/**
* Added Videos
* @description The video ids that were added to the board
*/
added_videos: string[];
};
/**
* Alpha Mask to Tensor
* @description Convert a mask image to a tensor. Opaque regions are 1 and transparent regions are 0.
@@ -2772,6 +2825,19 @@ export type components = {
*/
image_names: string[];
};
/** Body_add_videos_to_board */
Body_add_videos_to_board: {
/**
* Board Id
* @description The id of the board to add to
*/
board_id: string;
/**
* Video Ids
* @description The ids of the videos to add
*/
video_ids: string[];
};
/** Body_cancel_by_batch_ids */
Body_cancel_by_batch_ids: {
/**
@@ -2957,6 +3023,14 @@ export type components = {
*/
image_names: string[];
};
/** Body_remove_videos_from_board */
Body_remove_videos_from_board: {
/**
* Video Ids
* @description The ids of the videos to remove
*/
video_ids: string[];
};
/** Body_set_workflow_thumbnail */
Body_set_workflow_thumbnail: {
/**
@@ -18385,6 +18459,19 @@ export type components = {
*/
removed_images: string[];
};
/** RemoveVideosFromBoardResult */
RemoveVideosFromBoardResult: {
/**
* Affected Boards
* @description The ids of boards affected by the delete operation
*/
affected_boards: string[];
/**
* Removed Videos
* @description The video ids that were removed from their board
*/
removed_videos: string[];
};
/**
* Resize Latents
* @description Resizes latents to explicit width/height (in pixels). Provided dimensions are floor-divided by 8.
@@ -25148,6 +25235,72 @@ export interface operations {
};
};
};
add_videos_to_board: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["Body_add_videos_to_board"];
};
};
responses: {
/** @description Videos were added to board successfully */
201: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["AddVideosToBoardResult"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
remove_videos_from_board: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["Body_remove_videos_from_board"];
};
};
responses: {
/** @description Videos were removed from board successfully */
201: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["RemoveVideosFromBoardResult"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
get_related_models: {
parameters: {
query?: never;