Skip to content

Commit

Permalink
feat(ui): support videos modal
Browse files Browse the repository at this point in the history
  • Loading branch information
psychedelicious committed Nov 8, 2024
1 parent 5b3e159 commit ca8dce8
Show file tree
Hide file tree
Showing 8 changed files with 264 additions and 0 deletions.
60 changes: 60 additions & 0 deletions invokeai/frontend/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2109,5 +2109,65 @@
"readReleaseNotes": "Read Release Notes",
"watchRecentReleaseVideos": "Watch Recent Release Videos",
"watchUiUpdatesOverview": "Watch UI Updates Overview"
},
"supportVideos": {
"supportVideos": "Support Videos",
"gettingStarted": "Getting Started",
"controlCanvas": "Control Canvas",
"watchOnYoutube": "Watch on YouTube",
"videos": {
"creatingYourFirstImage": {
"title": "Creating Your First Image",
"description": "Introduction to creating an image from scratch using Invoke's tools."
},
"usingControlLayersAndReferenceGuides": {
"title": "Using Control Layers and Reference Guides",
"description": "Learn how to guide your image creation with control layers and reference images."
},
"understandingImageToImageAndDenoising": {
"title": "Understanding Image-to-Image and Denoising",
"description": "Overview of image-to-image transformations and denoising in Invoke."
},
"exploringAIModelsAndConceptAdapters": {
"title": "Exploring AI Models and Concept Adapters",
"description": "Dive into AI models and how to use concept adapters for creative control."
},
"creatingAndComposingOnInvokesControlCanvas": {
"title": "Creating and Composing on Invoke's Control Canvas",
"description": "Learn to compose images using Invoke's control canvas."
},
"upscaling": {
"title": "Upscaling",
"description": "How to upscale images with Invoke's tools to enhance resolution."
},
"howDoIGenerateAndSaveToTheGallery": {
"title": "How Do I Generate and Save to the Gallery?",
"description": "Steps to generate and save images to the gallery."
},
"howDoIEditOnTheCanvas": {
"title": "How Do I Edit on the Canvas?",
"description": "Guide to editing images directly on the canvas."
},
"howDoIDoImageToImageTransformation": {
"title": "How Do I Do Image-to-Image Transformation?",
"description": "Tutorial on performing image-to-image transformations in Invoke."
},
"howDoIUseControlNetsAndControlLayers": {
"title": "How Do I Use Control Nets and Control Layers?",
"description": "Learn to apply control layers and controlnets to your images."
},
"howDoIUseGlobalIPAdaptersAndReferenceImages": {
"title": "How Do I Use Global IP Adapters and Reference Images?",
"description": "Introduction to adding reference images and global IP adapters."
},
"howDoIUseInpaintMasks": {
"title": "How Do I Use Inpaint Masks?",
"description": "How to apply inpaint masks for image correction and variation."
},
"howDoIOutpaint": {
"title": "How Do I Outpaint?",
"description": "Guide to outpainting beyond the original image borders."
}
}
}
}
2 changes: 2 additions & 0 deletions invokeai/frontend/web/src/app/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/Cl
import { DeleteStylePresetDialog } from 'features/stylePresets/components/DeleteStylePresetDialog';
import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal';
import RefreshAfterResetModal from 'features/system/components/SettingsModal/RefreshAfterResetModal';
import { VideosModal } from 'features/system/components/VideosModal/VideosModal';
import { configChanged } from 'features/system/store/configSlice';
import { selectLanguage } from 'features/system/store/systemSelectors';
import { AppContent } from 'features/ui/components/AppContent';
Expand Down Expand Up @@ -108,6 +109,7 @@ const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => {
<NewCanvasSessionDialog />
<ImageContextMenu />
<FullscreenDropzone />
<VideosModal />
</ErrorBoundary>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ExternalLink, Flex, Spacer, Text } from '@invoke-ai/ui-library';
import type { VideoData } from 'features/system/components/VideosModal/data';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';

const formatTime = ({ minutes, seconds }: { minutes: number; seconds: number }) => {
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
};

export const VideoCard = memo(({ video }: { video: VideoData }) => {
const { t } = useTranslation();
const { tKey, link, length } = video;
return (
<Flex flexDir="column" gap={1}>
<Flex alignItems="center" gap={2}>
<Text fontSize="md" fontWeight="semibold">
{t(`supportVideos.videos.${tKey}.title`)}
</Text>
<Spacer />
<Text variant="subtext">{formatTime(length)}</Text>
<ExternalLink fontSize="sm" href={link} label={t('supportVideos.watchOnYoutube')} />
</Flex>
<Text fontSize="md" variant="subtext">
{t(`supportVideos.videos.${tKey}.description`)}
</Text>
</Flex>
);
});

VideoCard.displayName = 'VideoCard';
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Divider } from '@invoke-ai/ui-library';
import { StickyScrollable } from 'features/system/components/StickyScrollable';
import { gettingStartedVideos, type VideoData } from 'features/system/components/VideosModal/data';
import { VideoCard } from 'features/system/components/VideosModal/VideoCard';
import { Fragment, memo } from 'react';

export const VideoCardList = memo(({ category, videos }: { category: string; videos: VideoData[] }) => {
return (
<StickyScrollable title={category}>
{videos.map((video, i) => (
<Fragment key={`${video.tKey}-${i}`}>
<VideoCard video={video} />
{i < gettingStartedVideos.length - 1 && <Divider />}
</Fragment>
))}
</StickyScrollable>
);
});

VideoCardList.displayName = 'VideoCardList';
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {
Flex,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
} from '@invoke-ai/ui-library';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { buildUseDisclosure } from 'common/hooks/useBoolean';
import { controlCanvasVideos, gettingStartedVideos } from 'features/system/components/VideosModal/data';
import { VideoCardList } from 'features/system/components/VideosModal/VideoCardList';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';

export const [useVideosModal] = buildUseDisclosure(false);

export const VideosModal = memo(() => {
const { t } = useTranslation();
const videosModal = useVideosModal();

return (
<Modal isOpen={videosModal.isOpen} onClose={videosModal.close} size="2xl" isCentered useInert={false}>
<ModalOverlay />
<ModalContent maxH="80vh" h="80vh">
<ModalHeader bg="none">{t('supportVideos.supportVideos')}</ModalHeader>
<ModalCloseButton />
<ModalBody display="flex" flexDir="column" gap={4}>
<ScrollableContent>
<Flex flexDir="column" gap={4}>
<VideoCardList category={t('supportVideos.gettingStarted')} videos={gettingStartedVideos} />
<VideoCardList category={t('supportVideos.controlCanvas')} videos={controlCanvasVideos} />
</Flex>
</ScrollableContent>
</ModalBody>
<ModalFooter />
</ModalContent>
</Modal>
);
});

VideosModal.displayName = 'VideosModal';
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useVideosModal } from 'features/system/components/VideosModal/VideosModal';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiYoutubeLogoFill } from 'react-icons/pi';

export const VideosModalButton = memo(() => {
const { t } = useTranslation();
const videosModal = useVideosModal();
return (
<IconButton
aria-label={t('supportVideos.supportVideos')}
variant="link"
icon={<PiYoutubeLogoFill fontSize={20} />}
boxSize={8}
onClick={videosModal.open}
/>
);
});
VideosModalButton.displayName = 'VideosModalButton';
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* To add a support video, you'll need to add the video to the list below.
*
* The `tKey` is a sub-key in the translation file `invokeai/frontend/web/public/locales/en.json`.
* Add the title and description under `supportVideos.videos`, following the existing format.
*/

export type VideoData = {
tKey: string;
link: string;
length: {
minutes: number;
seconds: number;
};
};

export const gettingStartedVideos: VideoData[] = [
{
tKey: 'creatingYourFirstImage',
link: 'https://www.youtube.com/watch?v=jVi2XgSGrfY&list=PLvWK1Kc8iXGrQy8r9TYg6QdUuJ5MMx-ZO&index=1&t=29s&pp=iAQB',
length: { minutes: 6, seconds: 0 },
},
{
tKey: 'usingControlLayersAndReferenceGuides',
link: 'https://www.youtube.com/watch?v=crgw6bEgyrw&list=PLvWK1Kc8iXGrQy8r9TYg6QdUuJ5MMx-ZO&index=2&t=70s&pp=iAQB',
length: { minutes: 5, seconds: 30 },
},
{
tKey: 'understandingImageToImageAndDenoising',
link: 'https://www.youtube.com/watch?v=tvj8-0s6S2U&list=PLvWK1Kc8iXGrQy8r9TYg6QdUuJ5MMx-ZO&index=3&t=1s&pp=iAQB',
length: { minutes: 2, seconds: 37 },
},
{
tKey: 'exploringAIModelsAndConceptAdapters',
link: 'https://www.youtube.com/watch?v=iwBmBQMZ0UA&list=PLvWK1Kc8iXGrQy8r9TYg6QdUuJ5MMx-ZO&index=4&pp=iAQB',
length: { minutes: 8, seconds: 52 },
},
{
tKey: 'creatingAndComposingOnInvokesControlCanvas',
link: 'https://www.youtube.com/watch?v=MohWv5GZVGM&list=PLvWK1Kc8iXGrQy8r9TYg6QdUuJ5MMx-ZO&index=5&t=28s&pp=iAQB',
length: { minutes: 13, seconds: 56 },
},
{
tKey: 'upscaling',
link: 'https://www.youtube.com/watch?v=OCb19_P0nro&list=PLvWK1Kc8iXGrQy8r9TYg6QdUuJ5MMx-ZO&index=6&t=2s&pp=iAQB',
length: { minutes: 4, seconds: 0 },
},
];

export const controlCanvasVideos: VideoData[] = [
{
tKey: 'howDoIGenerateAndSaveToTheGallery',
link: 'https://youtu.be/Tl-69JvwJ2s?si=dbjmBc1iDAUpE1k5&t=26',
length: { minutes: 0, seconds: 49 },
},
{
tKey: 'howDoIEditOnTheCanvas',
link: 'https://youtu.be/Tl-69JvwJ2s?si=U_bFl9HsvSuejbxp&t=76',
length: { minutes: 0, seconds: 58 },
},
{
tKey: 'howDoIDoImageToImageTransformation',
link: 'https://youtu.be/Tl-69JvwJ2s?si=fjhTeY-yZ3qsEzEM&t=138',
length: { minutes: 0, seconds: 51 },
},
{
tKey: 'howDoIUseControlNetsAndControlLayers',
link: 'https://youtu.be/Tl-69JvwJ2s?si=x5KcYvkHbvR9ifsX&t=192',
length: { minutes: 1, seconds: 41 },
},
{
tKey: 'howDoIUseGlobalIPAdaptersAndReferenceImages',
link: 'https://youtu.be/Tl-69JvwJ2s?si=O940rNHiHGKXknK2&t=297',
length: { minutes: 0, seconds: 43 },
},
{
tKey: 'howDoIUseInpaintMasks',
link: 'https://youtu.be/Tl-69JvwJ2s?si=3DZhmerkzUmvJJSn&t=345',
length: { minutes: 1, seconds: 9 },
},
{
tKey: 'howDoIOutpaint',
link: 'https://youtu.be/Tl-69JvwJ2s?si=IIwkGZLq1PfLf80Q&t=420',
length: { minutes: 0, seconds: 48 },
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { $customNavComponent } from 'app/store/nanostores/customNavComponent';
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
import SettingsMenu from 'features/system/components/SettingsModal/SettingsMenu';
import StatusIndicator from 'features/system/components/StatusIndicator';
import { VideosModalButton } from 'features/system/components/VideosModal/VideosModalButton';
import { TabMountGate } from 'features/ui/components/TabMountGate';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
Expand Down Expand Up @@ -39,6 +40,7 @@ export const VerticalNavBar = memo(() => {
<Spacer />
<StatusIndicator />
<Notifications />
<VideosModalButton />
{customNavComponent ? customNavComponent : <SettingsMenu />}
</Flex>
);
Expand Down

0 comments on commit ca8dce8

Please sign in to comment.