mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
add option for video upsell, rearrange navigation bar and gallery tabs
This commit is contained in:
committed by
Mary Hipp Rogers
parent
0de5097207
commit
4845d31857
@@ -1280,7 +1280,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",
|
||||
@@ -2593,7 +2594,7 @@
|
||||
"panels": {
|
||||
"launchpad": "Launchpad",
|
||||
"workflowEditor": "Workflow Editor",
|
||||
"imageViewer": "Image Viewer",
|
||||
"imageViewer": "Viewer",
|
||||
"canvas": "Canvas",
|
||||
"video": "Video"
|
||||
},
|
||||
@@ -2602,6 +2603,15 @@
|
||||
"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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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: ['video'],
|
||||
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: {
|
||||
|
||||
@@ -82,14 +82,7 @@ export const GalleryPanel = memo(() => {
|
||||
>
|
||||
{t('parameters.images')}
|
||||
</Button>
|
||||
<Button
|
||||
tooltip={t('gallery.assetsTab')}
|
||||
onClick={handleClickAssets}
|
||||
data-testid="assets-tab"
|
||||
colorScheme={galleryView === 'assets' ? 'invokeBlue' : undefined}
|
||||
>
|
||||
{t('gallery.assets')}
|
||||
</Button>
|
||||
|
||||
{isVideoEnabled && (
|
||||
<Button
|
||||
tooltip={t('gallery.videosTab')}
|
||||
@@ -100,6 +93,14 @@ export const GalleryPanel = memo(() => {
|
||||
{t('gallery.videos')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
tooltip={t('gallery.assetsTab')}
|
||||
onClick={handleClickAssets}
|
||||
data-testid="assets-tab"
|
||||
colorScheme={galleryView === 'assets' ? 'invokeBlue' : undefined}
|
||||
>
|
||||
{t('gallery.assets')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Flex flexGrow={1} flexBasis={0} justifyContent="flex-end">
|
||||
<GalleryUploadButton />
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Box, Flex, forwardRef, Grid, GridItem, Spinner, Text } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { $accountTypeText } from 'app/store/nanostores/accountTypeText';
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { getFocusedRegion } from 'common/hooks/focus';
|
||||
import { useRangeBasedVideoFetching } from 'features/gallery/hooks/useRangeBasedVideoFetching';
|
||||
@@ -8,6 +10,7 @@ import type { selectGetVideoIdsQueryArgs } from 'features/gallery/store/galleryS
|
||||
import { selectGalleryImageMinimumWidth, selectLastSelectedItem } from 'features/gallery/store/gallerySelectors';
|
||||
import { selectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { selectAllowVideo } from 'features/system/store/configSlice';
|
||||
import type { MutableRefObject } from 'react';
|
||||
import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import type {
|
||||
@@ -287,6 +290,19 @@ export const VideoGallery = memo(() => {
|
||||
|
||||
const context = useMemo<GridContext>(() => ({ videoIds, queryArgs }), [videoIds, queryArgs]);
|
||||
|
||||
const isVideoEnabled = useAppSelector(selectAllowVideo);
|
||||
const accountTypeText = useStore($accountTypeText);
|
||||
|
||||
if (!isVideoEnabled) {
|
||||
return (
|
||||
<Flex w="full" h="full" alignItems="center" justifyContent="center" flexDirection="column" gap={4}>
|
||||
<Text fontSize="sm" color="base.300">
|
||||
Video generation is not enabled for {accountTypeText} accounts
|
||||
</Text>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Flex w="full" h="full" alignItems="center" justifyContent="center" gap={4}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { EMPTY_ARRAY } from 'app/store/constants';
|
||||
import { $accountTypeText } from 'app/store/nanostores/accountTypeText';
|
||||
import { $false } from 'app/store/nanostores/util';
|
||||
import type { AppDispatch, AppStore } from 'app/store/store';
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
@@ -39,7 +40,7 @@ import { selectVideoSlice, type VideoState } from 'features/parameters/store/vid
|
||||
import type { ParameterModel } from 'features/parameters/types/parameterSchemas';
|
||||
import { getGridSize } from 'features/parameters/util/optimalDimension';
|
||||
import { promptExpansionApi, type PromptExpansionRequestState } from 'features/prompt/PromptExpansion/state';
|
||||
import { selectConfigSlice } from 'features/system/store/configSlice';
|
||||
import { selectAllowVideo, selectConfigSlice } from 'features/system/store/configSlice';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import type { TabName } from 'features/ui/store/uiTypes';
|
||||
import i18n from 'i18next';
|
||||
@@ -93,6 +94,7 @@ const debouncedUpdateReasons = debounce(
|
||||
store: AppStore,
|
||||
isInPublishFlow: boolean,
|
||||
isChatGPT4oHighModelDisabled: (model: ParameterModel) => boolean,
|
||||
isVideoEnabled: boolean,
|
||||
promptExpansionRequest: PromptExpansionRequestState,
|
||||
video: VideoState
|
||||
) => {
|
||||
@@ -152,6 +154,7 @@ const debouncedUpdateReasons = debounce(
|
||||
params,
|
||||
promptExpansionRequest,
|
||||
dynamicPrompts,
|
||||
isVideoEnabled,
|
||||
});
|
||||
$reasonsWhyCannotEnqueue.set(reasons);
|
||||
} else {
|
||||
@@ -183,6 +186,7 @@ export const useReadinessWatcher = () => {
|
||||
const canvasIsCompositing = useStore(canvasManager?.compositor.$isBusy ?? $false);
|
||||
const isInPublishFlow = useStore($isInPublishFlow);
|
||||
const { isChatGPT4oHighModelDisabled } = useIsModelDisabled();
|
||||
const isVideoEnabled = useAppSelector(selectAllowVideo);
|
||||
const promptExpansionRequest = useStore(promptExpansionApi.$state);
|
||||
const video = useAppSelector(selectVideoSlice);
|
||||
useEffect(() => {
|
||||
@@ -206,6 +210,7 @@ export const useReadinessWatcher = () => {
|
||||
store,
|
||||
isInPublishFlow,
|
||||
isChatGPT4oHighModelDisabled,
|
||||
isVideoEnabled,
|
||||
promptExpansionRequest,
|
||||
video
|
||||
);
|
||||
@@ -229,6 +234,7 @@ export const useReadinessWatcher = () => {
|
||||
workflowSettings,
|
||||
isInPublishFlow,
|
||||
isChatGPT4oHighModelDisabled,
|
||||
isVideoEnabled,
|
||||
promptExpansionRequest,
|
||||
video,
|
||||
]);
|
||||
@@ -242,10 +248,16 @@ const getReasonsWhyCannotEnqueueVideoTab = (arg: {
|
||||
params: ParamsState;
|
||||
dynamicPrompts: DynamicPromptsState;
|
||||
promptExpansionRequest: PromptExpansionRequestState;
|
||||
isVideoEnabled: boolean;
|
||||
}) => {
|
||||
const { isConnected, video, params, dynamicPrompts, promptExpansionRequest } = arg;
|
||||
const { isConnected, video, params, dynamicPrompts, promptExpansionRequest, isVideoEnabled } = arg;
|
||||
const { positivePrompt } = params;
|
||||
const reasons: Reason[] = [];
|
||||
const accountTypeText = $accountTypeText.get();
|
||||
|
||||
if (!isVideoEnabled) {
|
||||
reasons.push({ content: i18n.t('parameters.invoke.videoIsDisabled', { accountType: accountTypeText }) });
|
||||
}
|
||||
|
||||
if (!isConnected) {
|
||||
reasons.push(disconnectedReason(i18n.t));
|
||||
|
||||
@@ -71,6 +71,8 @@ export const selectIsModelsTabDisabled = createConfigSelector((config) => config
|
||||
export const selectIsClientSideUploadEnabled = createConfigSelector((config) => config.allowClientSideUpload);
|
||||
export const selectAllowPublishWorkflows = createConfigSelector((config) => config.allowPublishWorkflows);
|
||||
export const selectAllowPromptExpansion = createConfigSelector((config) => config.allowPromptExpansion);
|
||||
export const selectAllowVideo = createConfigSelector((config) => config.allowVideo);
|
||||
|
||||
export const selectIsLocal = createSelector(selectConfigSlice, (config) => config.isLocal);
|
||||
export const selectShouldShowCredits = createConfigSelector((config) => config.shouldShowCredits);
|
||||
const selectDisabledTabs = createConfigSelector((config) => config.disabledTabs);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
||||
import { Divider, Flex, Spacer } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $customNavComponent } from 'app/store/nanostores/customNavComponent';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
@@ -48,13 +48,15 @@ export const VerticalNavBar = memo(() => {
|
||||
{withGenerateTab && <TabButton tab="generate" icon={<PiTextAaBold />} label={t('ui.tabs.generate')} />}
|
||||
{withCanvasTab && <TabButton tab="canvas" icon={<PiBoundingBoxBold />} label={t('ui.tabs.canvas')} />}
|
||||
{withUpscalingTab && <TabButton tab="upscaling" icon={<PiFrameCornersBold />} label={t('ui.tabs.upscaling')} />}
|
||||
{withWorkflowsTab && <TabButton tab="workflows" icon={<PiFlowArrowBold />} label={t('ui.tabs.workflows')} />}
|
||||
{withModelsTab && <TabButton tab="models" icon={<PiCubeBold />} label={t('ui.tabs.models')} />}
|
||||
{withQueueTab && <TabButton tab="queue" icon={<PiQueueBold />} label={t('ui.tabs.queue')} />}
|
||||
{withVideoTab && <TabButton tab="video" icon={<PiVideoBold />} label={t('ui.tabs.video')} />}
|
||||
{withWorkflowsTab && <TabButton tab="workflows" icon={<PiFlowArrowBold />} label={t('ui.tabs.workflows')} />}
|
||||
</Flex>
|
||||
<Spacer />
|
||||
<StatusIndicator />
|
||||
{withModelsTab && <TabButton tab="models" icon={<PiCubeBold />} label={t('ui.tabs.models')} />}
|
||||
{withQueueTab && <TabButton tab="queue" icon={<PiQueueBold />} label={t('ui.tabs.queue')} />}
|
||||
<Divider borderColor="base.200" />
|
||||
|
||||
<Notifications />
|
||||
<VideosModalButton />
|
||||
{customNavComponent ? customNavComponent : <SettingsMenu />}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppStore } from 'app/store/storeHooks';
|
||||
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||
import { imageDTOToImageWithDims } from 'features/controlLayers/store/util';
|
||||
import { videoFrameFromImageDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { startingFrameImageChanged } from 'features/parameters/store/videoSlice';
|
||||
import { LaunchpadButton } from 'features/ui/layouts/LaunchpadButton';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiUploadBold, PiVideoBold } from 'react-icons/pi';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
const dndTargetData = videoFrameFromImageDndTarget.getData({ frame: 'start' });
|
||||
|
||||
export const LaunchpadStartingFrameButton = memo((props: { extraAction?: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const { dispatch } = useAppStore();
|
||||
|
||||
const uploadOptions = useMemo(
|
||||
() =>
|
||||
({
|
||||
onUpload: (imageDTO: ImageDTO) => {
|
||||
dispatch(startingFrameImageChanged(imageDTOToImageWithDims(imageDTO)));
|
||||
props.extraAction?.();
|
||||
},
|
||||
allowMultiple: false,
|
||||
}) as const,
|
||||
[dispatch, props]
|
||||
);
|
||||
|
||||
const uploadApi = useImageUploadButton(uploadOptions);
|
||||
|
||||
return (
|
||||
<LaunchpadButton {...uploadApi.getUploadButtonProps()} position="relative" gap={8}>
|
||||
<Icon as={PiVideoBold} boxSize={8} color="base.500" />
|
||||
<Flex flexDir="column" alignItems="flex-start" gap={2}>
|
||||
<Heading size="sm">{t('ui.launchpad.addStartingFrame.title')}</Heading>
|
||||
<Text>{t('ui.launchpad.addStartingFrame.description')}</Text>
|
||||
</Flex>
|
||||
<Flex position="absolute" right={3} bottom={3}>
|
||||
<PiUploadBold />
|
||||
<input {...uploadApi.getUploadInputProps()} />
|
||||
</Flex>
|
||||
<DndDropTarget dndTarget={videoFrameFromImageDndTarget} dndTargetData={dndTargetData} label="Drop" />
|
||||
</LaunchpadButton>
|
||||
);
|
||||
});
|
||||
|
||||
LaunchpadStartingFrameButton.displayName = 'LaunchpadStartingFrameButton';
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Button, Flex, Grid, Text } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $videoUpsellComponent } from 'app/store/nanostores/videoUpsellComponent';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { VideoModelPicker } from 'features/settingsAccordions/components/VideoSettingsAccordion/VideoModelPicker';
|
||||
import { selectAllowVideo } from 'features/system/store/configSlice';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { LaunchpadContainer } from './LaunchpadContainer';
|
||||
import { LaunchpadGenerateFromTextButton } from './LaunchpadGenerateFromTextButton';
|
||||
import { LaunchpadStartingFrameButton } from './LaunchpadStartingFrameButton';
|
||||
|
||||
export const VideoLaunchpadPanel = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const isVideoEnabled = useAppSelector(selectAllowVideo);
|
||||
const videoUpsellComponent = useStore($videoUpsellComponent);
|
||||
|
||||
if (!isVideoEnabled) {
|
||||
return (
|
||||
<LaunchpadContainer heading={t('ui.launchpad.videoTitle')}>
|
||||
<Grid gridTemplateColumns="1fr 1fr" gap={8}>
|
||||
{videoUpsellComponent}
|
||||
</Grid>
|
||||
</LaunchpadContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<LaunchpadContainer heading={t('ui.launchpad.videoTitle')}>
|
||||
<Grid gridTemplateColumns="1fr 1fr" gap={8}>
|
||||
<VideoModelPicker />
|
||||
<Flex flexDir="column" gap={2} justifyContent="center">
|
||||
<Text>
|
||||
{t('ui.launchpad.modelGuideText')}{' '}
|
||||
<Button
|
||||
as="a"
|
||||
variant="link"
|
||||
href="https://support.invoke.ai/support/solutions/articles/151000216086-model-guide"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
size="sm"
|
||||
>
|
||||
{t('ui.launchpad.modelGuideLink')}
|
||||
</Button>
|
||||
</Text>
|
||||
</Flex>
|
||||
</Grid>
|
||||
<LaunchpadGenerateFromTextButton />
|
||||
<LaunchpadStartingFrameButton />
|
||||
</LaunchpadContainer>
|
||||
);
|
||||
});
|
||||
VideoLaunchpadPanel.displayName = 'VideoLaunchpadPanel';
|
||||
@@ -21,7 +21,6 @@ import { memo, useCallback, useEffect } from 'react';
|
||||
import { DockviewTab } from './DockviewTab';
|
||||
import { DockviewTabLaunchpad } from './DockviewTabLaunchpad';
|
||||
import { DockviewTabProgress } from './DockviewTabProgress';
|
||||
import { GenerateLaunchpadPanel } from './GenerateLaunchpadPanel';
|
||||
import { navigationApi } from './navigation-api';
|
||||
import { PanelHotkeysLogical } from './PanelHotkeysLogical';
|
||||
import {
|
||||
@@ -43,6 +42,7 @@ import {
|
||||
SETTINGS_PANEL_ID,
|
||||
VIEWER_PANEL_ID,
|
||||
} from './shared';
|
||||
import { VideoLaunchpadPanel } from './VideoLaunchpadPanel';
|
||||
import { VideoTabLeftPanel } from './VideoTabLeftPanel';
|
||||
|
||||
const tabComponents = {
|
||||
@@ -52,7 +52,7 @@ const tabComponents = {
|
||||
};
|
||||
|
||||
const mainPanelComponents: AutoLayoutDockviewComponents = {
|
||||
[LAUNCHPAD_PANEL_ID]: withPanelContainer(GenerateLaunchpadPanel),
|
||||
[LAUNCHPAD_PANEL_ID]: withPanelContainer(VideoLaunchpadPanel),
|
||||
[VIEWER_PANEL_ID]: withPanelContainer(ImageViewerPanel),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user