Skip to content

Commit 87d74b9

Browse files
feat(ui): support videos modal
1 parent 7ad1c29 commit 87d74b9

File tree

8 files changed

+264
-0
lines changed

8 files changed

+264
-0
lines changed

invokeai/frontend/web/public/locales/en.json

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2124,5 +2124,65 @@
21242124
"readReleaseNotes": "Read Release Notes",
21252125
"watchRecentReleaseVideos": "Watch Recent Release Videos",
21262126
"watchUiUpdatesOverview": "Watch UI Updates Overview"
2127+
},
2128+
"supportVideos": {
2129+
"supportVideos": "Support Videos",
2130+
"gettingStarted": "Getting Started",
2131+
"controlCanvas": "Control Canvas",
2132+
"watchOnYoutube": "Watch on YouTube",
2133+
"videos": {
2134+
"creatingYourFirstImage": {
2135+
"title": "Creating Your First Image",
2136+
"description": "Introduction to creating an image from scratch using Invoke's tools."
2137+
},
2138+
"usingControlLayersAndReferenceGuides": {
2139+
"title": "Using Control Layers and Reference Guides",
2140+
"description": "Learn how to guide your image creation with control layers and reference images."
2141+
},
2142+
"understandingImageToImageAndDenoising": {
2143+
"title": "Understanding Image-to-Image and Denoising",
2144+
"description": "Overview of image-to-image transformations and denoising in Invoke."
2145+
},
2146+
"exploringAIModelsAndConceptAdapters": {
2147+
"title": "Exploring AI Models and Concept Adapters",
2148+
"description": "Dive into AI models and how to use concept adapters for creative control."
2149+
},
2150+
"creatingAndComposingOnInvokesControlCanvas": {
2151+
"title": "Creating and Composing on Invoke's Control Canvas",
2152+
"description": "Learn to compose images using Invoke's control canvas."
2153+
},
2154+
"upscaling": {
2155+
"title": "Upscaling",
2156+
"description": "How to upscale images with Invoke's tools to enhance resolution."
2157+
},
2158+
"howDoIGenerateAndSaveToTheGallery": {
2159+
"title": "How Do I Generate and Save to the Gallery?",
2160+
"description": "Steps to generate and save images to the gallery."
2161+
},
2162+
"howDoIEditOnTheCanvas": {
2163+
"title": "How Do I Edit on the Canvas?",
2164+
"description": "Guide to editing images directly on the canvas."
2165+
},
2166+
"howDoIDoImageToImageTransformation": {
2167+
"title": "How Do I Do Image-to-Image Transformation?",
2168+
"description": "Tutorial on performing image-to-image transformations in Invoke."
2169+
},
2170+
"howDoIUseControlNetsAndControlLayers": {
2171+
"title": "How Do I Use Control Nets and Control Layers?",
2172+
"description": "Learn to apply control layers and controlnets to your images."
2173+
},
2174+
"howDoIUseGlobalIPAdaptersAndReferenceImages": {
2175+
"title": "How Do I Use Global IP Adapters and Reference Images?",
2176+
"description": "Introduction to adding reference images and global IP adapters."
2177+
},
2178+
"howDoIUseInpaintMasks": {
2179+
"title": "How Do I Use Inpaint Masks?",
2180+
"description": "How to apply inpaint masks for image correction and variation."
2181+
},
2182+
"howDoIOutpaint": {
2183+
"title": "How Do I Outpaint?",
2184+
"description": "Guide to outpainting beyond the original image borders."
2185+
}
2186+
}
21272187
}
21282188
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/Cl
2727
import { DeleteStylePresetDialog } from 'features/stylePresets/components/DeleteStylePresetDialog';
2828
import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal';
2929
import RefreshAfterResetModal from 'features/system/components/SettingsModal/RefreshAfterResetModal';
30+
import { VideosModal } from 'features/system/components/VideosModal/VideosModal';
3031
import { configChanged } from 'features/system/store/configSlice';
3132
import { selectLanguage } from 'features/system/store/systemSelectors';
3233
import { AppContent } from 'features/ui/components/AppContent';
@@ -108,6 +109,7 @@ const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => {
108109
<NewCanvasSessionDialog />
109110
<ImageContextMenu />
110111
<FullscreenDropzone />
112+
<VideosModal />
111113
</ErrorBoundary>
112114
);
113115
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { ExternalLink, Flex, Spacer, Text } from '@invoke-ai/ui-library';
2+
import type { VideoData } from 'features/system/components/VideosModal/data';
3+
import { memo } from 'react';
4+
import { useTranslation } from 'react-i18next';
5+
6+
const formatTime = ({ minutes, seconds }: { minutes: number; seconds: number }) => {
7+
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
8+
};
9+
10+
export const VideoCard = memo(({ video }: { video: VideoData }) => {
11+
const { t } = useTranslation();
12+
const { tKey, link, length } = video;
13+
return (
14+
<Flex flexDir="column" gap={1}>
15+
<Flex alignItems="center" gap={2}>
16+
<Text fontSize="md" fontWeight="semibold">
17+
{t(`supportVideos.videos.${tKey}.title`)}
18+
</Text>
19+
<Spacer />
20+
<Text variant="subtext">{formatTime(length)}</Text>
21+
<ExternalLink fontSize="sm" href={link} label={t('supportVideos.watchOnYoutube')} />
22+
</Flex>
23+
<Text fontSize="md" variant="subtext">
24+
{t(`supportVideos.videos.${tKey}.description`)}
25+
</Text>
26+
</Flex>
27+
);
28+
});
29+
30+
VideoCard.displayName = 'VideoCard';
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Divider } from '@invoke-ai/ui-library';
2+
import { StickyScrollable } from 'features/system/components/StickyScrollable';
3+
import { gettingStartedVideos, type VideoData } from 'features/system/components/VideosModal/data';
4+
import { VideoCard } from 'features/system/components/VideosModal/VideoCard';
5+
import { Fragment, memo } from 'react';
6+
7+
export const VideoCardList = memo(({ category, videos }: { category: string; videos: VideoData[] }) => {
8+
return (
9+
<StickyScrollable title={category}>
10+
{videos.map((video, i) => (
11+
<Fragment key={`${video.tKey}-${i}`}>
12+
<VideoCard video={video} />
13+
{i < gettingStartedVideos.length - 1 && <Divider />}
14+
</Fragment>
15+
))}
16+
</StickyScrollable>
17+
);
18+
});
19+
20+
VideoCardList.displayName = 'VideoCardList';
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {
2+
Flex,
3+
Modal,
4+
ModalBody,
5+
ModalCloseButton,
6+
ModalContent,
7+
ModalFooter,
8+
ModalHeader,
9+
ModalOverlay,
10+
} from '@invoke-ai/ui-library';
11+
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
12+
import { buildUseDisclosure } from 'common/hooks/useBoolean';
13+
import { controlCanvasVideos, gettingStartedVideos } from 'features/system/components/VideosModal/data';
14+
import { VideoCardList } from 'features/system/components/VideosModal/VideoCardList';
15+
import { memo } from 'react';
16+
import { useTranslation } from 'react-i18next';
17+
18+
export const [useVideosModal] = buildUseDisclosure(false);
19+
20+
export const VideosModal = memo(() => {
21+
const { t } = useTranslation();
22+
const videosModal = useVideosModal();
23+
24+
return (
25+
<Modal isOpen={videosModal.isOpen} onClose={videosModal.close} size="2xl" isCentered useInert={false}>
26+
<ModalOverlay />
27+
<ModalContent maxH="80vh" h="80vh">
28+
<ModalHeader bg="none">{t('supportVideos.supportVideos')}</ModalHeader>
29+
<ModalCloseButton />
30+
<ModalBody display="flex" flexDir="column" gap={4}>
31+
<ScrollableContent>
32+
<Flex flexDir="column" gap={4}>
33+
<VideoCardList category={t('supportVideos.gettingStarted')} videos={gettingStartedVideos} />
34+
<VideoCardList category={t('supportVideos.controlCanvas')} videos={controlCanvasVideos} />
35+
</Flex>
36+
</ScrollableContent>
37+
</ModalBody>
38+
<ModalFooter />
39+
</ModalContent>
40+
</Modal>
41+
);
42+
});
43+
44+
VideosModal.displayName = 'VideosModal';
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { IconButton } from '@invoke-ai/ui-library';
2+
import { useVideosModal } from 'features/system/components/VideosModal/VideosModal';
3+
import { memo } from 'react';
4+
import { useTranslation } from 'react-i18next';
5+
import { PiYoutubeLogoFill } from 'react-icons/pi';
6+
7+
export const VideosModalButton = memo(() => {
8+
const { t } = useTranslation();
9+
const videosModal = useVideosModal();
10+
return (
11+
<IconButton
12+
aria-label={t('supportVideos.supportVideos')}
13+
variant="link"
14+
icon={<PiYoutubeLogoFill fontSize={20} />}
15+
boxSize={8}
16+
onClick={videosModal.open}
17+
/>
18+
);
19+
});
20+
VideosModalButton.displayName = 'VideosModalButton';
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* To add a support video, you'll need to add the video to the list below.
3+
*
4+
* The `tKey` is a sub-key in the translation file `invokeai/frontend/web/public/locales/en.json`.
5+
* Add the title and description under `supportVideos.videos`, following the existing format.
6+
*/
7+
8+
export type VideoData = {
9+
tKey: string;
10+
link: string;
11+
length: {
12+
minutes: number;
13+
seconds: number;
14+
};
15+
};
16+
17+
export const gettingStartedVideos: VideoData[] = [
18+
{
19+
tKey: 'creatingYourFirstImage',
20+
link: 'https://www.youtube.com/watch?v=jVi2XgSGrfY&list=PLvWK1Kc8iXGrQy8r9TYg6QdUuJ5MMx-ZO&index=1&t=29s&pp=iAQB',
21+
length: { minutes: 6, seconds: 0 },
22+
},
23+
{
24+
tKey: 'usingControlLayersAndReferenceGuides',
25+
link: 'https://www.youtube.com/watch?v=crgw6bEgyrw&list=PLvWK1Kc8iXGrQy8r9TYg6QdUuJ5MMx-ZO&index=2&t=70s&pp=iAQB',
26+
length: { minutes: 5, seconds: 30 },
27+
},
28+
{
29+
tKey: 'understandingImageToImageAndDenoising',
30+
link: 'https://www.youtube.com/watch?v=tvj8-0s6S2U&list=PLvWK1Kc8iXGrQy8r9TYg6QdUuJ5MMx-ZO&index=3&t=1s&pp=iAQB',
31+
length: { minutes: 2, seconds: 37 },
32+
},
33+
{
34+
tKey: 'exploringAIModelsAndConceptAdapters',
35+
link: 'https://www.youtube.com/watch?v=iwBmBQMZ0UA&list=PLvWK1Kc8iXGrQy8r9TYg6QdUuJ5MMx-ZO&index=4&pp=iAQB',
36+
length: { minutes: 8, seconds: 52 },
37+
},
38+
{
39+
tKey: 'creatingAndComposingOnInvokesControlCanvas',
40+
link: 'https://www.youtube.com/watch?v=MohWv5GZVGM&list=PLvWK1Kc8iXGrQy8r9TYg6QdUuJ5MMx-ZO&index=5&t=28s&pp=iAQB',
41+
length: { minutes: 13, seconds: 56 },
42+
},
43+
{
44+
tKey: 'upscaling',
45+
link: 'https://www.youtube.com/watch?v=OCb19_P0nro&list=PLvWK1Kc8iXGrQy8r9TYg6QdUuJ5MMx-ZO&index=6&t=2s&pp=iAQB',
46+
length: { minutes: 4, seconds: 0 },
47+
},
48+
];
49+
50+
export const controlCanvasVideos: VideoData[] = [
51+
{
52+
tKey: 'howDoIGenerateAndSaveToTheGallery',
53+
link: 'https://youtu.be/Tl-69JvwJ2s?si=dbjmBc1iDAUpE1k5&t=26',
54+
length: { minutes: 0, seconds: 49 },
55+
},
56+
{
57+
tKey: 'howDoIEditOnTheCanvas',
58+
link: 'https://youtu.be/Tl-69JvwJ2s?si=U_bFl9HsvSuejbxp&t=76',
59+
length: { minutes: 0, seconds: 58 },
60+
},
61+
{
62+
tKey: 'howDoIDoImageToImageTransformation',
63+
link: 'https://youtu.be/Tl-69JvwJ2s?si=fjhTeY-yZ3qsEzEM&t=138',
64+
length: { minutes: 0, seconds: 51 },
65+
},
66+
{
67+
tKey: 'howDoIUseControlNetsAndControlLayers',
68+
link: 'https://youtu.be/Tl-69JvwJ2s?si=x5KcYvkHbvR9ifsX&t=192',
69+
length: { minutes: 1, seconds: 41 },
70+
},
71+
{
72+
tKey: 'howDoIUseGlobalIPAdaptersAndReferenceImages',
73+
link: 'https://youtu.be/Tl-69JvwJ2s?si=O940rNHiHGKXknK2&t=297',
74+
length: { minutes: 0, seconds: 43 },
75+
},
76+
{
77+
tKey: 'howDoIUseInpaintMasks',
78+
link: 'https://youtu.be/Tl-69JvwJ2s?si=3DZhmerkzUmvJJSn&t=345',
79+
length: { minutes: 1, seconds: 9 },
80+
},
81+
{
82+
tKey: 'howDoIOutpaint',
83+
link: 'https://youtu.be/Tl-69JvwJ2s?si=IIwkGZLq1PfLf80Q&t=420',
84+
length: { minutes: 0, seconds: 48 },
85+
},
86+
];

invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { $customNavComponent } from 'app/store/nanostores/customNavComponent';
44
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
55
import SettingsMenu from 'features/system/components/SettingsModal/SettingsMenu';
66
import StatusIndicator from 'features/system/components/StatusIndicator';
7+
import { VideosModalButton } from 'features/system/components/VideosModal/VideosModalButton';
78
import { TabMountGate } from 'features/ui/components/TabMountGate';
89
import { memo } from 'react';
910
import { useTranslation } from 'react-i18next';
@@ -39,6 +40,7 @@ export const VerticalNavBar = memo(() => {
3940
<Spacer />
4041
<StatusIndicator />
4142
<Notifications />
43+
<VideosModalButton />
4244
{customNavComponent ? customNavComponent : <SettingsMenu />}
4345
</Flex>
4446
);

0 commit comments

Comments
 (0)