video metadata support

This commit is contained in:
Mary Hipp
2025-08-22 15:55:43 -04:00
committed by psychedelicious
parent 2b688ed855
commit 32e2d176de
20 changed files with 292 additions and 43 deletions

View File

@@ -0,0 +1,90 @@
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 { MetadataHandlers } 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 ImageMetadataActions, { UnrecallableMetadataDatum } 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={MetadataHandlers.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.imageDetails')}</Tab>
<Tab>{t('metadata.workflow')}</Tab>
<Tab>{t('nodes.graph')}</Tab>
</TabList>
<TabPanels>
<TabPanel>
{isLoading && <IAINoContentFallbackWithSpinner label="Loading metadata..." />}
{metadata && !isLoading && (
<ScrollableContent>
<ImageMetadataActions metadata={metadata} />
</ScrollableContent>
)}
{!metadata && !isLoading && <IAINoContentFallback label={t('metadata.noRecallParameters')} />}
</TabPanel>
<TabPanel>
{metadata ? (
<DataViewer
fileName={`${video.video_id.replace('.png', '')}_metadata`}
data={metadata}
label={t('metadata.metadata')}
/>
) : (
<IAINoContentFallback label={t('metadata.noMetaData')} />
)}
</TabPanel>
<TabPanel>
{video ? (
<DataViewer
fileName={`${video.video_id.replace('.png', '')}_details`}
data={video}
label={t('metadata.imageDetails')}
/>
) : (
<IAINoContentFallback label={t('metadata.noImageDetails')} />
)}
</TabPanel>
{/* <TabPanel>
<ImageMetadataWorkflowTabContent image={image} />
</TabPanel>
<TabPanel>
<ImageMetadataGraphTabContent image={image} />
</TabPanel> */}
</TabPanels>
</Tabs>
</Flex>
);
};
export default memo(VideoMetadataViewer);

View File

@@ -5,7 +5,7 @@ import { CanvasAlertsInvocationProgress } from 'features/controlLayers/component
import { DndImage } from 'features/dnd/DndImage';
import ImageMetadataViewer from 'features/gallery/components/ImageMetadataViewer/ImageMetadataViewer';
import NextPrevItemButtons from 'features/gallery/components/NextPrevItemButtons';
import { selectShouldShowImageDetails, selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors';
import { selectShouldShowItemDetails, selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors';
import type { AnimationProps } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion';
import { memo, useCallback, useRef, useState } from 'react';
@@ -17,7 +17,7 @@ import { ProgressImage } from './ProgressImage2';
import { ProgressIndicator } from './ProgressIndicator2';
export const CurrentImagePreview = memo(({ imageDTO }: { imageDTO: ImageDTO | null }) => {
const shouldShowImageDetails = useAppSelector(selectShouldShowImageDetails);
const shouldShowItemDetails = useAppSelector(selectShouldShowItemDetails);
const shouldShowProgressInViewer = useAppSelector(selectShouldShowProgressInViewer);
const { onLoadImage, $progressEvent, $progressImage } = useImageViewerContext();
const progressEvent = useStore($progressEvent);
@@ -65,7 +65,7 @@ export const CurrentImagePreview = memo(({ imageDTO }: { imageDTO: ImageDTO | nu
<Flex flexDir="column" gap={2} position="absolute" top={0} insetInlineStart={0} alignItems="flex-start">
<CanvasAlertsInvocationProgress />
</Flex>
{shouldShowImageDetails && imageDTO && !withProgress && (
{shouldShowItemDetails && imageDTO && !withProgress && (
<Box position="absolute" opacity={0.8} top={0} width="full" height="full" borderRadius="base">
<ImageMetadataViewer image={imageDTO} />
</Box>

View File

@@ -0,0 +1,85 @@
import { Box, Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import VideoMetadataViewer from 'features/gallery/components/ImageMetadataViewer/VideoMetadataViewer';
import NextPrevItemButtons from 'features/gallery/components/NextPrevItemButtons';
import { selectShouldShowItemDetails } from 'features/ui/store/uiSelectors';
import { VideoPlayer } from 'features/video/components/VideoPlayer';
import type { AnimationProps } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion';
import { memo, useCallback, useRef, useState } from 'react';
import type { VideoDTO } from 'services/api/types';
import { NoContentForViewer } from './NoContentForViewer';
export const CurrentVideoPreview = memo(({ videoDTO }: { videoDTO: VideoDTO | null }) => {
const shouldShowItemDetails = useAppSelector(selectShouldShowItemDetails);
// Show and hide the next/prev buttons on mouse move
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] = useState<boolean>(false);
const timeoutId = useRef(0);
const onMouseOver = useCallback(() => {
setShouldShowNextPrevButtons(true);
window.clearTimeout(timeoutId.current);
}, []);
const onMouseOut = useCallback(() => {
timeoutId.current = window.setTimeout(() => {
setShouldShowNextPrevButtons(false);
}, 500);
}, []);
return (
<Flex
onMouseOver={onMouseOver}
onMouseOut={onMouseOut}
width="full"
height="full"
alignItems="center"
justifyContent="center"
position="relative"
>
{videoDTO && videoDTO.video_url && (
<Flex w="full" h="full" position="absolute" alignItems="center" justifyContent="center">
<VideoPlayer />
</Flex>
)}
{!videoDTO && <NoContentForViewer />}
{shouldShowItemDetails && videoDTO && (
<Box position="absolute" opacity={0.8} top={0} width="full" height="full" borderRadius="base">
<VideoMetadataViewer video={videoDTO} />
</Box>
)}
<AnimatePresence>
{shouldShowNextPrevButtons && videoDTO && (
<Box
as={motion.div}
key="nextPrevButtons"
initial={initial}
animate={animateArrows}
exit={exit}
position="absolute"
top={0}
right={0}
bottom={0}
left={0}
pointerEvents="none"
>
<NextPrevItemButtons />
</Box>
)}
</AnimatePresence>
</Flex>
);
});
CurrentVideoPreview.displayName = 'CurrentVideoPreview';
const initial: AnimationProps['initial'] = {
opacity: 0,
};
const animateArrows: AnimationProps['animate'] = {
opacity: 1,
transition: { duration: 0.07 },
};
const exit: AnimationProps['exit'] = {
opacity: 0,
transition: { duration: 0.07 },
};

View File

@@ -8,7 +8,7 @@ import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { useImageDTO } from 'services/api/endpoints/images';
import { ViewerToolbar } from './ViewerToolbar';
import { ImageViewerToolbar } from './ImageViewerToolbar';
const dndTargetData = setComparisonImageDndTarget.getData();
@@ -19,7 +19,7 @@ export const ImageViewer = memo(() => {
const lastSelectedImageDTO = useImageDTO(lastSelectedItem?.type === 'image' ? lastSelectedItem.id : null);
return (
<Flex flexDir="column" w="full" h="full" overflow="hidden" gap={2} position="relative">
<ViewerToolbar />
<ImageViewerToolbar />
<Divider />
<Flex w="full" h="full" position="relative">
<CurrentImagePreview imageDTO={lastSelectedImageDTO} />

View File

@@ -1,12 +1,12 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectImageToCompare, selectLastSelectedItem } from 'features/gallery/store/gallerySelectors';
import { VideoPlayer } from 'features/video/components/VideoPlayer';
import { memo } from 'react';
import { ImageViewerContextProvider } from './context';
import { ImageComparison } from './ImageComparison';
import { ImageViewer } from './ImageViewer';
import { VideoViewer } from './VideoViewer';
const selectIsComparing = createSelector(
[selectLastSelectedItem, selectImageToCompare],
@@ -20,7 +20,7 @@ export const ImageViewerPanel = memo(() => {
return (
<ImageViewerContextProvider>
{!isComparing && lastSelectedItem?.type === 'image' && <ImageViewer />}
{!isComparing && lastSelectedItem?.type === 'video' && <VideoPlayer />}
{!isComparing && lastSelectedItem?.type === 'video' && <VideoViewer />}
{isComparing && <ImageComparison />}
</ImageViewerContextProvider>
);

View File

@@ -8,7 +8,7 @@ import { useImageDTO } from 'services/api/endpoints/images';
import { CurrentImageButtons } from './CurrentImageButtons';
import { ToggleProgressButton } from './ToggleProgressButton';
export const ViewerToolbar = memo(() => {
export const ImageViewerToolbar = memo(() => {
const lastSelectedItem = useAppSelector(selectLastSelectedItem);
const imageDTO = useImageDTO(lastSelectedItem?.id);
@@ -23,4 +23,4 @@ export const ViewerToolbar = memo(() => {
);
});
ViewerToolbar.displayName = 'ViewerToolbar';
ImageViewerToolbar.displayName = 'ImageViewerToolbar';

View File

@@ -3,8 +3,8 @@ import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectLastSelectedItem } from 'features/gallery/store/gallerySelectors';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { selectShouldShowImageDetails, selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors';
import { setShouldShowImageDetails } from 'features/ui/store/uiSlice';
import { selectShouldShowItemDetails, selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors';
import { setShouldShowItemDetails } from 'features/ui/store/uiSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiInfoBold } from 'react-icons/pi';
@@ -19,19 +19,19 @@ export const ToggleMetadataViewerButton = memo(() => {
const isDisabledOverride = hasProgressImage && shouldShowProgressInViewer;
const shouldShowImageDetails = useAppSelector(selectShouldShowImageDetails);
const shouldShowItemDetails = useAppSelector(selectShouldShowItemDetails);
const imageDTO = useAppSelector(selectLastSelectedItem);
const { t } = useTranslation();
const toggleMetadataViewer = useCallback(() => {
dispatch(setShouldShowImageDetails(!shouldShowImageDetails));
}, [dispatch, shouldShowImageDetails]);
dispatch(setShouldShowItemDetails(!shouldShowItemDetails));
}, [dispatch, shouldShowItemDetails]);
useRegisteredHotkeys({
id: 'toggleMetadata',
category: 'viewer',
callback: toggleMetadataViewer,
dependencies: [imageDTO, shouldShowImageDetails],
dependencies: [imageDTO, shouldShowItemDetails],
});
return (
@@ -42,7 +42,7 @@ export const ToggleMetadataViewerButton = memo(() => {
onClick={toggleMetadataViewer}
variant="link"
alignSelf="stretch"
colorScheme={shouldShowImageDetails ? 'invokeBlue' : 'base'}
colorScheme={shouldShowItemDetails ? 'invokeBlue' : 'base'}
data-testid="toggle-show-metadata-button"
isDisabled={isDisabledOverride}
/>

View File

@@ -0,0 +1,24 @@
import { Divider, Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { selectLastSelectedItem } from 'features/gallery/store/gallerySelectors';
import { memo } from 'react';
import { useVideoDTO } from 'services/api/endpoints/videos';
import { CurrentVideoPreview } from './CurrentVideoPreview';
import { VideoViewerToolbar } from './VideoViewerToolbar';
export const VideoViewer = memo(() => {
const lastSelectedItem = useAppSelector(selectLastSelectedItem);
const videoDTO = useVideoDTO(lastSelectedItem?.type === 'video' ? lastSelectedItem.id : null);
return (
<Flex flexDir="column" w="full" h="full" overflow="hidden" gap={2} position="relative">
<VideoViewerToolbar />
<Divider />
<Flex w="full" h="full" position="relative">
<CurrentVideoPreview videoDTO={videoDTO ?? null} />
</Flex>
</Flex>
);
});
VideoViewer.displayName = 'VideoViewer';

View File

@@ -0,0 +1,19 @@
import { Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton';
import { selectLastSelectedItem } from 'features/gallery/store/gallerySelectors';
import { memo } from 'react';
import { useVideoDTO } from 'services/api/endpoints/videos';
export const VideoViewerToolbar = memo(() => {
const lastSelectedItem = useAppSelector(selectLastSelectedItem);
const videoDTO = useVideoDTO(lastSelectedItem?.type === 'video' ? lastSelectedItem.id : null);
return (
<Flex w="full" justifyContent="flex-end" h={8}>
{videoDTO && <ToggleMetadataViewerButton />}
</Flex>
);
});
VideoViewerToolbar.displayName = 'VideoViewerToolbar';

View File

@@ -4,7 +4,7 @@ import { fieldRunwayModelValueChanged } from 'features/nodes/store/nodesSlice';
import type { RunwayModelFieldInputInstance, RunwayModelFieldInputTemplate } from 'features/nodes/types/field';
import { memo, useCallback } from 'react';
import { useRunwayModels } from 'services/api/hooks/modelsByType';
import type { VideoApiModelConfig } from 'services/api/types';
import type { VideoApiModelConfig } from 'services/api/types';
import type { FieldComponentProps } from './types';

View File

@@ -8,6 +8,7 @@ import type { GraphBuilderArg, GraphBuilderReturn } from 'features/nodes/util/gr
import { UnsupportedGenerationModeError } from 'features/nodes/util/graph/types';
import { selectStartingFrameImage, selectVideoSlice } from 'features/parameters/store/videoSlice';
import { t } from 'i18next';
import type { VideoApiModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
const log = logger('system');
@@ -33,7 +34,9 @@ export const buildRunwayVideoGraph = (arg: GraphBuilderArg): GraphBuilderReturn
const firstFrameImageField = zImageField.parse(startingFrameImage);
const { seed, shouldRandomizeSeed } = params;
const { videoDuration } = videoParams;
const { videoModel, videoDuration, videoAspectRatio, videoResolution } = videoParams;
assert(videoModel, 'Runway video requires a model');
const finalSeed = shouldRandomizeSeed ? undefined : seed;
@@ -51,7 +54,7 @@ export const buildRunwayVideoGraph = (arg: GraphBuilderArg): GraphBuilderReturn
// @ts-expect-error: This node is not available in the OSS application
type: 'runway_generate_video',
duration: parseInt(videoDuration || '0', 10),
aspect_ratio: params.dimensions.aspectRatio.id,
aspect_ratio: videoAspectRatio,
seed: finalSeed,
first_frame_image: firstFrameImageField,
});
@@ -61,12 +64,13 @@ export const buildRunwayVideoGraph = (arg: GraphBuilderArg): GraphBuilderReturn
// Set up metadata
g.upsertMetadata({
model: Graph.getModelMetadataField(videoModel as VideoApiModelConfig),
positive_prompt: prompts.positive,
negative_prompt: prompts.negative || '',
video_duration: videoDuration,
video_aspect_ratio: params.dimensions.aspectRatio.id,
duration: videoDuration,
aspect_ratio: videoAspectRatio,
resolution: videoResolution,
seed: finalSeed,
generation_type: 'image-to-video',
first_frame_image: startingFrameImage,
});

View File

@@ -8,6 +8,7 @@ import type { GraphBuilderArg, GraphBuilderReturn } from 'features/nodes/util/gr
import { UnsupportedGenerationModeError } from 'features/nodes/util/graph/types';
import { selectStartingFrameImage, selectVideoSlice } from 'features/parameters/store/videoSlice';
import { t } from 'i18next';
import type { VideoApiModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
const log = logger('system');
@@ -28,9 +29,11 @@ export const buildVeo3VideoGraph = (arg: GraphBuilderArg): GraphBuilderReturn =>
assert(prompts.positive.length > 0, 'Veo3 video requires positive prompt to have at least one character');
const { seed, shouldRandomizeSeed } = params;
const { videoModel, videoResolution, videoDuration } = videoParams;
const { videoModel, videoResolution, videoDuration, videoAspectRatio } = videoParams;
const finalSeed = shouldRandomizeSeed ? undefined : seed;
assert(videoModel, 'Veo3 video requires a model');
const g = new Graph(getPrefixedId('veo3_video_graph'));
const positivePrompt = g.addNode({
@@ -63,14 +66,14 @@ export const buildVeo3VideoGraph = (arg: GraphBuilderArg): GraphBuilderReturn =>
// Set up metadata
g.upsertMetadata({
model: Graph.getModelMetadataField(videoModel as VideoApiModelConfig),
positive_prompt: prompts.positive,
negative_prompt: prompts.negative || '',
video_duration: videoDuration,
video_aspect_ratio: params.dimensions.aspectRatio.id,
duration: videoDuration,
aspect_ratio: videoAspectRatio,
resolution: videoResolution,
seed: finalSeed,
generation_type: 'image-to-video',
starting_image: startingFrameImage,
video_model: videoParams.videoModel,
first_frame_image: startingFrameImage,
});
g.setMetadataReceivingNode(veo3VideoNode);

View File

@@ -16,9 +16,9 @@ import {
zChatGPT4oAspectRatioID,
zFluxKontextAspectRatioID,
zGemini2_5AspectRatioID,
zImagen3AspectRatioID,
zRunwayAspectRatioID,
zVeo3AspectRatioID,
zImagen3AspectRatioID,
} from 'features/controlLayers/store/types';
import { selectIsRunway, selectIsVeo3 } from 'features/parameters/store/videoSlice';
import type { ChangeEventHandler } from 'react';

View File

@@ -2,6 +2,6 @@ import { createSelector } from '@reduxjs/toolkit';
import { selectUiSlice } from 'features/ui/store/uiSlice';
export const selectActiveTab = createSelector(selectUiSlice, (ui) => ui.activeTab);
export const selectShouldShowImageDetails = createSelector(selectUiSlice, (ui) => ui.shouldShowImageDetails);
export const selectShouldShowItemDetails = createSelector(selectUiSlice, (ui) => ui.shouldShowItemDetails);
export const selectShouldShowProgressInViewer = createSelector(selectUiSlice, (ui) => ui.shouldShowProgressInViewer);
export const selectPickerCompactViewStates = createSelector(selectUiSlice, (ui) => ui.pickerCompactViewStates);

View File

@@ -14,8 +14,8 @@ const slice = createSlice({
setActiveTab: (state, action: PayloadAction<UIState['activeTab']>) => {
state.activeTab = action.payload;
},
setShouldShowImageDetails: (state, action: PayloadAction<UIState['shouldShowImageDetails']>) => {
state.shouldShowImageDetails = action.payload;
setShouldShowItemDetails: (state, action: PayloadAction<UIState['shouldShowItemDetails']>) => {
state.shouldShowItemDetails = action.payload;
},
setShouldShowProgressInViewer: (state, action: PayloadAction<UIState['shouldShowProgressInViewer']>) => {
state.shouldShowProgressInViewer = action.payload;
@@ -75,7 +75,7 @@ const slice = createSlice({
export const {
setActiveTab,
setShouldShowImageDetails,
setShouldShowItemDetails,
setShouldShowProgressInViewer,
accordionStateChanged,
expanderStateChanged,
@@ -111,6 +111,6 @@ export const uiSliceConfig: SliceConfig<typeof slice> = {
}
return zUIState.parse(state);
},
persistDenylist: ['shouldShowImageDetails'],
persistDenylist: ['shouldShowItemDetails'],
},
};

View File

@@ -15,7 +15,7 @@ export type Serializable = z.infer<typeof zSerializable>;
export const zUIState = z.object({
_version: z.literal(4),
activeTab: zTabName,
shouldShowImageDetails: z.boolean(),
shouldShowItemDetails: z.boolean(),
shouldShowProgressInViewer: z.boolean(),
accordions: z.record(z.string(), z.boolean()),
expanders: z.record(z.string(), z.boolean()),
@@ -28,7 +28,7 @@ export type UIState = z.infer<typeof zUIState>;
export const getInitialUIState = (): UIState => ({
_version: 4 as const,
activeTab: 'generate' as const,
shouldShowImageDetails: false,
shouldShowItemDetails: false,
shouldShowProgressInViewer: true,
accordions: {},
expanders: {},

View File

@@ -1,18 +1,15 @@
import { Flex, Text } from '@invoke-ai/ui-library';
import { skipToken } from '@reduxjs/toolkit/query';
import { Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useFocusRegion } from 'common/hooks/focus';
import { selectLastSelectedItem } from 'features/gallery/store/gallerySelectors';
import { memo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import ReactPlayer from 'react-player';
import { useGetVideoDTOQuery } from 'services/api/endpoints/videos';
import { useVideoDTO } from 'services/api/endpoints/videos';
export const VideoPlayer = memo(() => {
const { t } = useTranslation();
const ref = useRef<HTMLDivElement>(null);
const lastSelectedItem = useAppSelector(selectLastSelectedItem);
const { data: videoDTO } = useGetVideoDTOQuery(lastSelectedItem?.id ?? skipToken);
const videoDTO = useVideoDTO(lastSelectedItem?.id);
useFocusRegion('video', ref);
@@ -21,7 +18,6 @@ export const VideoPlayer = memo(() => {
{videoDTO?.video_url && (
<ReactPlayer src={videoDTO.video_url} controls={true} width={videoDTO.width} height={videoDTO.height} />
)}
{!videoDTO?.video_url && <Text>{t('gallery.noVideoSelected')}</Text>}
</Flex>
);
});

View File

@@ -1,3 +1,4 @@
import { skipToken } from '@reduxjs/toolkit/query';
import { getStore } from 'app/store/nanostores/store';
import type { paths } from 'services/api/schema';
import type { GetVideoIdsArgs, GetVideoIdsResult, VideoDTO } from 'services/api/types';
@@ -7,6 +8,7 @@ import {
} from 'services/api/util/tagInvalidation';
import stableHash from 'stable-hash';
import type { Param0 } from 'tsafe';
import type { JsonObject } from 'type-fest';
import { api, buildV1Url, LIST_TAG } from '..';
@@ -32,6 +34,11 @@ export const videosApi = api.injectEndpoints({
providesTags: (result, error, video_id) => [{ type: 'Video', id: video_id }],
}),
getVideoMetadata: build.query<JsonObject | undefined, string>({
query: (video_id) => ({ url: buildVideosUrl(`i/${video_id}/metadata`) }),
providesTags: (result, error, video_id) => [{ type: 'VideoMetadata', id: video_id }],
}),
/**
* Get ordered list of image names for selection operations
*/
@@ -201,6 +208,7 @@ export const {
useDeleteVideosMutation,
useAddVideosToBoardMutation,
useRemoveVideosFromBoardMutation,
useGetVideoMetadataQuery,
} = videosApi;
/**
@@ -224,3 +232,8 @@ export const getVideoDTOSafe = async (
return null;
}
};
export const useVideoDTO = (video_id: string | null | undefined) => {
const { currentData: videoDTO } = useGetVideoDTOQuery(video_id ?? skipToken);
return videoDTO ?? null;
};

View File

@@ -2,6 +2,7 @@ import { skipToken } from '@reduxjs/toolkit/query';
import { useAppSelector } from 'app/store/storeHooks';
import { selectMetadataFetchDebounce } from 'features/system/store/configSlice';
import { imagesApi, useGetImageMetadataQuery } from 'services/api/endpoints/images';
import { useGetVideoMetadataQuery, videosApi } from 'services/api/endpoints/videos';
import { useDebounce } from 'use-debounce';
export const useDebouncedMetadata = (imageName?: string | null) => {
@@ -16,3 +17,16 @@ export const useDebouncedMetadata = (imageName?: string | null) => {
isLoading: cachedData ? false : isFetching || imageName !== debouncedImageName,
};
};
export const useDebouncedVideoMetadata = (videoId?: string | null) => {
const metadataFetchDebounce = useAppSelector(selectMetadataFetchDebounce);
const [debouncedVideoId] = useDebounce(videoId, metadataFetchDebounce);
const { currentData: cachedData } = videosApi.endpoints.getVideoMetadata.useQueryState(videoId ?? skipToken);
const { currentData: data, isFetching } = useGetVideoMetadataQuery(debouncedVideoId ?? skipToken);
return {
metadata: cachedData ?? data,
isLoading: cachedData ? false : isFetching || videoId !== debouncedVideoId,
};
};

View File

@@ -55,6 +55,7 @@ const tagTypes = [
'Schema',
'QueueCountsByDestination',
'Video',
'VideoMetadata',
'VideoList',
'VideoIdList',
'VideoCollectionCounts',