diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 35644728938..218c92471ab 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -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." + } + } } } diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index 67003f5b62e..2902c344adb 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -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'; @@ -108,6 +109,7 @@ const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => { + ); }; diff --git a/invokeai/frontend/web/src/features/system/components/VideosModal/VideoCard.tsx b/invokeai/frontend/web/src/features/system/components/VideosModal/VideoCard.tsx new file mode 100644 index 00000000000..66e204c90dd --- /dev/null +++ b/invokeai/frontend/web/src/features/system/components/VideosModal/VideoCard.tsx @@ -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 ( + + + + {t(`supportVideos.videos.${tKey}.title`)} + + + {formatTime(length)} + + + + {t(`supportVideos.videos.${tKey}.description`)} + + + ); +}); + +VideoCard.displayName = 'VideoCard'; diff --git a/invokeai/frontend/web/src/features/system/components/VideosModal/VideoCardList.tsx b/invokeai/frontend/web/src/features/system/components/VideosModal/VideoCardList.tsx new file mode 100644 index 00000000000..4140b05c03d --- /dev/null +++ b/invokeai/frontend/web/src/features/system/components/VideosModal/VideoCardList.tsx @@ -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 ( + + {videos.map((video, i) => ( + + + {i < gettingStartedVideos.length - 1 && } + + ))} + + ); +}); + +VideoCardList.displayName = 'VideoCardList'; diff --git a/invokeai/frontend/web/src/features/system/components/VideosModal/VideosModal.tsx b/invokeai/frontend/web/src/features/system/components/VideosModal/VideosModal.tsx new file mode 100644 index 00000000000..fb8ae679390 --- /dev/null +++ b/invokeai/frontend/web/src/features/system/components/VideosModal/VideosModal.tsx @@ -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 ( + + + + {t('supportVideos.supportVideos')} + + + + + + + + + + + + + ); +}); + +VideosModal.displayName = 'VideosModal'; diff --git a/invokeai/frontend/web/src/features/system/components/VideosModal/VideosModalButton.tsx b/invokeai/frontend/web/src/features/system/components/VideosModal/VideosModalButton.tsx new file mode 100644 index 00000000000..7436b8ec7f7 --- /dev/null +++ b/invokeai/frontend/web/src/features/system/components/VideosModal/VideosModalButton.tsx @@ -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 ( + } + boxSize={8} + onClick={videosModal.open} + /> + ); +}); +VideosModalButton.displayName = 'VideosModalButton'; diff --git a/invokeai/frontend/web/src/features/system/components/VideosModal/data.ts b/invokeai/frontend/web/src/features/system/components/VideosModal/data.ts new file mode 100644 index 00000000000..5f88500dfff --- /dev/null +++ b/invokeai/frontend/web/src/features/system/components/VideosModal/data.ts @@ -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 }, + }, +]; diff --git a/invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx b/invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx index 2dbb87e8b50..a5925d8de58 100644 --- a/invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx +++ b/invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx @@ -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'; @@ -39,6 +40,7 @@ export const VerticalNavBar = memo(() => { + {customNavComponent ? customNavComponent : } );