diff --git a/backend/src/bundles/templates/enums/enums.ts b/backend/src/bundles/templates/enums/enums.ts index f85dd91b5..161821e35 100644 --- a/backend/src/bundles/templates/enums/enums.ts +++ b/backend/src/bundles/templates/enums/enums.ts @@ -1,2 +1,2 @@ export { templateErrorMessage } from './template-error-message.enum.js'; -export { templateApiPath } from './templates-api-path.enum.js'; +export { TemplateApiPath } from 'shared'; diff --git a/backend/src/bundles/templates/templates.controller.ts b/backend/src/bundles/templates/templates.controller.ts index c52672322..0d539a5f2 100644 --- a/backend/src/bundles/templates/templates.controller.ts +++ b/backend/src/bundles/templates/templates.controller.ts @@ -7,7 +7,7 @@ import { ApiPath } from '~/common/enums/enums.js'; import { HTTPCode, HTTPMethod } from '~/common/http/http.js'; import { type Logger } from '~/common/logger/logger.js'; -import { templateApiPath } from './enums/enums.js'; +import { TemplateApiPath } from './enums/enums.js'; import { type TemplateService } from './templates.service.js'; import { type CreateTemplateRequestDto, @@ -111,7 +111,7 @@ class TemplateController extends BaseController { this.templateService = templateService; this.addRoute({ - path: templateApiPath.USER, + path: TemplateApiPath.USER, method: HTTPMethod.GET, handler: (options) => this.findAllByUser( @@ -122,7 +122,7 @@ class TemplateController extends BaseController { }); this.addRoute({ - path: templateApiPath.PUBLIC, + path: TemplateApiPath.PUBLIC, method: HTTPMethod.GET, handler: () => { return this.findPublicTemplates(); @@ -130,7 +130,7 @@ class TemplateController extends BaseController { }); this.addRoute({ - path: templateApiPath.ID, + path: TemplateApiPath.ID, method: HTTPMethod.GET, handler: (options) => { return this.findById( @@ -142,7 +142,7 @@ class TemplateController extends BaseController { }); this.addRoute({ - path: templateApiPath.ROOT, + path: TemplateApiPath.ROOT, method: HTTPMethod.POST, validation: { body: createTemplateValidationSchema, @@ -156,7 +156,7 @@ class TemplateController extends BaseController { }); this.addRoute({ - path: templateApiPath.ID, + path: TemplateApiPath.ID, method: HTTPMethod.PATCH, validation: { body: updateTemplateValidationSchema, @@ -172,7 +172,7 @@ class TemplateController extends BaseController { }); this.addRoute({ - path: templateApiPath.ID, + path: TemplateApiPath.ID, method: HTTPMethod.DELETE, handler: (options) => this.delete( diff --git a/frontend/src/bundles/common/middlewares/draft.middleware.ts b/frontend/src/bundles/common/middlewares/draft.middleware.ts index f1c2d9995..59e8f712b 100644 --- a/frontend/src/bundles/common/middlewares/draft.middleware.ts +++ b/frontend/src/bundles/common/middlewares/draft.middleware.ts @@ -14,6 +14,8 @@ const CHANGE_DRAFT_ACTIONS = new Set([ studioActions.resizeScene.type, studioActions.setVideoName.type, studioActions.setVideoSize.type, + studioActions.addBackgroundToScene.type, + studioActions.loadTemplate.type, ]); const isChangeDraftAction = (action: Action): boolean => { diff --git a/frontend/src/bundles/studio/components/video-menu/components/menu-content/backgrounds-content/backgrounds-content.tsx b/frontend/src/bundles/studio/components/video-menu/components/menu-content/backgrounds-content/backgrounds-content.tsx index b0cc6af44..c780726e1 100644 --- a/frontend/src/bundles/studio/components/video-menu/components/menu-content/backgrounds-content/backgrounds-content.tsx +++ b/frontend/src/bundles/studio/components/video-menu/components/menu-content/backgrounds-content/backgrounds-content.tsx @@ -14,7 +14,7 @@ import backgroundColors from '~/bundles/studio/data/bg-colors.json'; import backgroundImages from '~/bundles/studio/data/bg-images.json'; import { actions as studioActions } from '~/bundles/studio/store/studio.js'; -import { ColorCard, ImageCard } from './components/components.js'; +import { BackgroundImageCard, ColorCard } from './components/components.js'; import styles from './styles.module.css'; const BackgroundsContent: React.FC = () => { @@ -43,7 +43,7 @@ const BackgroundsContent: React.FC = () => { {backgroundImages.map((imageSource, index) => ( - diff --git a/frontend/src/bundles/studio/components/video-menu/components/menu-content/backgrounds-content/components/components.ts b/frontend/src/bundles/studio/components/video-menu/components/menu-content/backgrounds-content/components/components.ts index f699b1c2e..f4a76ce49 100644 --- a/frontend/src/bundles/studio/components/video-menu/components/menu-content/backgrounds-content/components/components.ts +++ b/frontend/src/bundles/studio/components/video-menu/components/menu-content/backgrounds-content/components/components.ts @@ -1,2 +1,2 @@ export { ColorCard } from './color-card.js'; -export { ImageCard } from './image-card.js'; +export { BackgroundImageCard } from './image-card.js'; diff --git a/frontend/src/bundles/studio/components/video-menu/components/menu-content/backgrounds-content/components/image-card.tsx b/frontend/src/bundles/studio/components/video-menu/components/menu-content/backgrounds-content/components/image-card.tsx index bc8348e52..9bf020cd8 100644 --- a/frontend/src/bundles/studio/components/video-menu/components/menu-content/backgrounds-content/components/image-card.tsx +++ b/frontend/src/bundles/studio/components/video-menu/components/menu-content/backgrounds-content/components/image-card.tsx @@ -1,14 +1,12 @@ -import { Box, Image } from '~/bundles/common/components/components.js'; import { useAppDispatch, useCallback } from '~/bundles/common/hooks/hooks.js'; +import { ImageCard } from '~/bundles/studio/components/video-menu/components/menu-content/components/components.js'; import { actions as studioActions } from '~/bundles/studio/store/studio.js'; -import styles from './styles.module.css'; - type Properties = { imageSource: string; }; -const ImageCard: React.FC = ({ imageSource }) => { +const BackgroundImageCard: React.FC = ({ imageSource }) => { const dispatch = useAppDispatch(); const handleImageClick = useCallback((): void => { @@ -20,16 +18,7 @@ const ImageCard: React.FC = ({ imageSource }) => { ); }, [dispatch, imageSource]); - return ( - - - - ); + return ; }; -export { ImageCard }; +export { BackgroundImageCard }; diff --git a/frontend/src/bundles/studio/components/video-menu/components/menu-content/backgrounds-content/components/styles.module.css b/frontend/src/bundles/studio/components/video-menu/components/menu-content/backgrounds-content/components/styles.module.css index add5f9567..fe90fe7cf 100644 --- a/frontend/src/bundles/studio/components/video-menu/components/menu-content/backgrounds-content/components/styles.module.css +++ b/frontend/src/bundles/studio/components/video-menu/components/menu-content/backgrounds-content/components/styles.module.css @@ -1,12 +1,3 @@ -.image-item { - height: 100px; - background-color: var(--chakra-colors-background-700); - border-radius: 7px; - border-width: 1px; - border-color: transparent; - transition: all 0.3s ease; -} - .color-item { border-radius: 7px; height: 80px; @@ -15,12 +6,6 @@ transition: all 0.3s ease; } -.image-item:hover { - background-color: var(--chakra-colors-background-600); - border-color: var(--chakra-colors-brand-secondary-300); - cursor: pointer; -} - .color-item:hover { border-color: var(--chakra-colors-brand-secondary-300); cursor: pointer; diff --git a/frontend/src/bundles/studio/components/video-menu/components/menu-content/components/components.ts b/frontend/src/bundles/studio/components/video-menu/components/menu-content/components/components.ts new file mode 100644 index 000000000..53d3191ca --- /dev/null +++ b/frontend/src/bundles/studio/components/video-menu/components/menu-content/components/components.ts @@ -0,0 +1 @@ +export { ImageCard } from './image-card/image-card.js'; diff --git a/frontend/src/bundles/studio/components/video-menu/components/menu-content/components/image-card/image-card.tsx b/frontend/src/bundles/studio/components/video-menu/components/menu-content/components/image-card/image-card.tsx new file mode 100644 index 000000000..1960c610a --- /dev/null +++ b/frontend/src/bundles/studio/components/video-menu/components/menu-content/components/image-card/image-card.tsx @@ -0,0 +1,23 @@ +import { Box, Image } from '~/bundles/common/components/components.js'; + +import styles from './styles.module.css'; + +type Properties = { + imageSource: string; + onClick?: () => void; +}; + +const ImageCard: React.FC = ({ imageSource, onClick }) => { + return ( + + + + ); +}; + +export { ImageCard }; diff --git a/frontend/src/bundles/studio/components/video-menu/components/menu-content/components/image-card/styles.module.css b/frontend/src/bundles/studio/components/video-menu/components/menu-content/components/image-card/styles.module.css new file mode 100644 index 000000000..e6407a873 --- /dev/null +++ b/frontend/src/bundles/studio/components/video-menu/components/menu-content/components/image-card/styles.module.css @@ -0,0 +1,14 @@ +.image-item { + height: 100px; + background-color: var(--chakra-colors-background-700); + border-radius: 7px; + border-width: 1px; + border-color: transparent; + transition: all 0.3s ease; +} + +.image-item:hover { + background-color: var(--chakra-colors-background-600); + border-color: var(--chakra-colors-brand-secondary-300); + cursor: pointer; +} diff --git a/frontend/src/bundles/studio/components/video-menu/components/menu-content/content.ts b/frontend/src/bundles/studio/components/video-menu/components/menu-content/content.ts index fee93901e..ad57e9cdd 100644 --- a/frontend/src/bundles/studio/components/video-menu/components/menu-content/content.ts +++ b/frontend/src/bundles/studio/components/video-menu/components/menu-content/content.ts @@ -1,3 +1,4 @@ export { AvatarsContent } from './avatars-content/avatars-content.js'; export { BackgroundsContent } from './backgrounds-content/backgrounds-content.js'; export { ScriptContent } from './script-content/script-content.js'; +export { TemplatesContent } from './templates-content/templates-content.js'; diff --git a/frontend/src/bundles/studio/components/video-menu/components/menu-content/templates-content/components/components.ts b/frontend/src/bundles/studio/components/video-menu/components/menu-content/templates-content/components/components.ts new file mode 100644 index 000000000..a302af5fc --- /dev/null +++ b/frontend/src/bundles/studio/components/video-menu/components/menu-content/templates-content/components/components.ts @@ -0,0 +1 @@ +export { TemplateCard } from './template-card.js'; diff --git a/frontend/src/bundles/studio/components/video-menu/components/menu-content/templates-content/components/template-card.tsx b/frontend/src/bundles/studio/components/video-menu/components/menu-content/templates-content/components/template-card.tsx new file mode 100644 index 000000000..a100d8fb2 --- /dev/null +++ b/frontend/src/bundles/studio/components/video-menu/components/menu-content/templates-content/components/template-card.tsx @@ -0,0 +1,22 @@ +import { useAppDispatch, useCallback } from '~/bundles/common/hooks/hooks.js'; +import { ImageCard } from '~/bundles/studio/components/video-menu/components/menu-content/components/components.js'; +import { actions as studioActions } from '~/bundles/studio/store/studio.js'; +import { type Template } from '~/bundles/studio/types/types.js'; + +type Properties = { + template: Template; +}; + +const TemplateCard: React.FC = ({ template }) => { + const dispatch = useAppDispatch(); + + const handleClick = useCallback((): void => { + void dispatch(studioActions.loadTemplate(template)); + }, [dispatch, template]); + + return ( + + ); +}; + +export { TemplateCard }; diff --git a/frontend/src/bundles/studio/components/video-menu/components/menu-content/templates-content/templates-content.tsx b/frontend/src/bundles/studio/components/video-menu/components/menu-content/templates-content/templates-content.tsx new file mode 100644 index 000000000..5f3879918 --- /dev/null +++ b/frontend/src/bundles/studio/components/video-menu/components/menu-content/templates-content/templates-content.tsx @@ -0,0 +1,86 @@ +import { + Box, + Loader, + SimpleGrid, + Tab, + TabList, + TabPanel, + TabPanels, + Tabs, + Text, +} from '~/bundles/common/components/components.js'; +import { EMPTY_VALUE } from '~/bundles/common/constants/constants.js'; +import { DataStatus } from '~/bundles/common/enums/data-status.enum.js'; +import { + useAppDispatch, + useAppSelector, + useEffect, +} from '~/bundles/common/hooks/hooks.js'; +import { actions as studioActions } from '~/bundles/studio/store/studio.js'; + +import { TemplateCard } from './components/components.js'; + +const TemplatesContent: React.FC = () => { + const dispatch = useAppDispatch(); + + const { templates, dataStatus } = useAppSelector(({ studio }) => studio); + + useEffect(() => { + if (templates.public.length === EMPTY_VALUE) { + void dispatch(studioActions.loadPublicTemplates()); + } + if (!templates.isUserLoaded) { + void dispatch(studioActions.loadUserTemplates()); + } + }, [dispatch, templates]); + + return ( + + + Templates + My templates + + + {dataStatus === DataStatus.PENDING ? ( + + + + ) : ( + + + + {templates.public.map((template) => ( + + ))} + + + + {templates.user.length > EMPTY_VALUE ? ( + + {templates.user.map((template) => ( + + ))} + + ) : ( + + You have no templates yet. + + )} + + + )} + + ); +}; + +export { TemplatesContent }; diff --git a/frontend/src/bundles/studio/components/video-menu/video-menu.tsx b/frontend/src/bundles/studio/components/video-menu/video-menu.tsx index ff30bed1c..021dbe1e7 100644 --- a/frontend/src/bundles/studio/components/video-menu/video-menu.tsx +++ b/frontend/src/bundles/studio/components/video-menu/video-menu.tsx @@ -19,6 +19,7 @@ import { AvatarsContent, BackgroundsContent, ScriptContent, + TemplatesContent, } from './components/menu-content/content.js'; // import { // AssetsContent, @@ -66,11 +67,11 @@ const VideoMenu: React.FC = () => { // TODO: Uncomment menu items after demo const menuItems: Record, MenuItem> = { - // templates: { - // label: 'Templates', - // icon: , - // getContent: () => , - // }, + templates: { + label: 'Templates', + icon: , + getContent: () => , + }, avatars: { label: 'Avatars', icon: , diff --git a/frontend/src/bundles/studio/constants/constants.ts b/frontend/src/bundles/studio/constants/constants.ts index 31bc043be..e673e7175 100644 --- a/frontend/src/bundles/studio/constants/constants.ts +++ b/frontend/src/bundles/studio/constants/constants.ts @@ -10,6 +10,8 @@ export { MIN_SCENE_DURATION } from './min-scene-duration.constant.js'; export { NEW_SCRIPT_TEXT } from './new-script-text.constant.js'; export { SCRIPT_AND_AVATAR_ARE_REQUIRED } from './script-and-avatar-are-required.constant.js'; export { SKIP_TO_PREV_SCENE_THRESHOLD } from './skip-to-previous-scene-threshold.constant.js'; +export { TEMPLATE_SAVE_FAILED_NOTOFICATION_ID } from './template-save-failed-notification-id.js'; +export { TEMPLATE_SAVE_NOTOFICATION_ID } from './template-save-notification-id.js'; export { VIDEO_SAVE_FAILED_NOTIFICATION_ID } from './video-save-failed-notification-id.js'; export { VIDEO_SAVE_NOTIFICATION_ID } from './video-save-notification-id.js'; export { VIDEO_SUBMIT_FAILED_NOTIFICATION_ID } from './video-submit-failed-id.constant.js'; diff --git a/frontend/src/bundles/studio/constants/template-save-failed-notification-id.ts b/frontend/src/bundles/studio/constants/template-save-failed-notification-id.ts new file mode 100644 index 000000000..c0549068e --- /dev/null +++ b/frontend/src/bundles/studio/constants/template-save-failed-notification-id.ts @@ -0,0 +1,3 @@ +const TEMPLATE_SAVE_FAILED_NOTOFICATION_ID = 'template-save-failed'; + +export { TEMPLATE_SAVE_FAILED_NOTOFICATION_ID }; diff --git a/frontend/src/bundles/studio/constants/template-save-notification-id.ts b/frontend/src/bundles/studio/constants/template-save-notification-id.ts new file mode 100644 index 000000000..3b3bef74a --- /dev/null +++ b/frontend/src/bundles/studio/constants/template-save-notification-id.ts @@ -0,0 +1,3 @@ +const TEMPLATE_SAVE_NOTOFICATION_ID = 'template-save'; + +export { TEMPLATE_SAVE_NOTOFICATION_ID }; diff --git a/frontend/src/bundles/studio/enums/enums.ts b/frontend/src/bundles/studio/enums/enums.ts index 7787da3d4..f2fc60aff 100644 --- a/frontend/src/bundles/studio/enums/enums.ts +++ b/frontend/src/bundles/studio/enums/enums.ts @@ -3,4 +3,4 @@ export { NotificationMessage } from './notification-message.enum.js'; export { NotificationTitle } from './notification-title.enum.js'; export { PlayIconNames } from './play-icon-names.enum.js'; export { RowNames } from './row-names.enum.js'; -export { AvatarsApiPath, SpeechApiPath } from 'shared'; +export { AvatarsApiPath, SpeechApiPath, TemplateApiPath } from 'shared'; diff --git a/frontend/src/bundles/studio/enums/menu-items.enum.ts b/frontend/src/bundles/studio/enums/menu-items.enum.ts index f64cda74d..ccbfe0e73 100644 --- a/frontend/src/bundles/studio/enums/menu-items.enum.ts +++ b/frontend/src/bundles/studio/enums/menu-items.enum.ts @@ -1,6 +1,6 @@ // TODO: Uncomment menu items after demo const MenuItems = { - // TEMPLATES: 'templates', + TEMPLATES: 'templates', AVATARS: 'avatars', SCRIPT: 'script', // TEXT: 'text', diff --git a/frontend/src/bundles/studio/enums/notification-message.enum.ts b/frontend/src/bundles/studio/enums/notification-message.enum.ts index ce2c28620..78c9518e9 100644 --- a/frontend/src/bundles/studio/enums/notification-message.enum.ts +++ b/frontend/src/bundles/studio/enums/notification-message.enum.ts @@ -5,6 +5,8 @@ const NotificationMessage = { 'To create a video, you need to have an avatar and a script.', VIDEO_SAVE: 'Video saved successfully', VIDEO_SAVE_FAILED: 'Video save failed', + TEMPLATE_SAVE: 'Template saved successfully', + TEMPLATE_SAVE_FAILED: 'Template save failed', } as const; export { NotificationMessage }; diff --git a/frontend/src/bundles/studio/enums/notification-title.enum.ts b/frontend/src/bundles/studio/enums/notification-title.enum.ts index cf73f0ea2..0e7b5f6cb 100644 --- a/frontend/src/bundles/studio/enums/notification-title.enum.ts +++ b/frontend/src/bundles/studio/enums/notification-title.enum.ts @@ -4,6 +4,8 @@ const NotificationTitle = { SCRIPT_AND_AVATAR_ARE_REQUIRED: 'Script and avatar are required', VIDEO_SAVED: 'Video saved', VIDEO_SAVE_FAILED: 'Video save failed', + TEMPLATE_SAVED: 'Template saved', + TEMPLATE_SAVE_FAILED: 'Template save failed', } as const; export { NotificationTitle }; diff --git a/frontend/src/bundles/studio/pages/studio.tsx b/frontend/src/bundles/studio/pages/studio.tsx index 15dd38ece..1bcc59757 100644 --- a/frontend/src/bundles/studio/pages/studio.tsx +++ b/frontend/src/bundles/studio/pages/studio.tsx @@ -40,6 +40,8 @@ import { } from '../components/components.js'; import { SCRIPT_AND_AVATAR_ARE_REQUIRED, + TEMPLATE_SAVE_FAILED_NOTOFICATION_ID, + TEMPLATE_SAVE_NOTOFICATION_ID, VIDEO_SAVE_FAILED_NOTIFICATION_ID, VIDEO_SAVE_NOTIFICATION_ID, VIDEO_SUBMIT_FAILED_NOTIFICATION_ID, @@ -51,7 +53,10 @@ import { getVoicesConfigs, scenesExceedScripts, } from '../helpers/helpers.js'; -import { selectVideoDataById } from '../store/selectors.js'; +import { + selectTemplateDataById, + selectVideoDataById, +} from '../store/selectors.js'; import { actions as studioActions } from '../store/studio.js'; const Studio: React.FC = () => { @@ -62,6 +67,10 @@ const Studio: React.FC = () => { selectVideoDataById(state, locationState?.id), ); + const templateData = useAppSelector((state) => + selectTemplateDataById(state, locationState?.templateId), + ); + const { scenes, scripts, @@ -89,7 +98,10 @@ const Studio: React.FC = () => { if (videoData) { void dispatch(studioActions.loadVideoData(videoData)); } - }, [dispatch, videoData]); + if (templateData) { + void dispatch(studioActions.loadTemplate(templateData)); + } + }, [dispatch, templateData, videoData]); const handleResize = useCallback(() => { dispatch(studioActions.changeVideoSize()); @@ -193,6 +205,24 @@ const Studio: React.FC = () => { [dispatch], ); + const handleSaveTemplate = useCallback((): void => { + void dispatch(studioActions.createTemplate()) + .then(() => { + notificationService.success({ + id: TEMPLATE_SAVE_NOTOFICATION_ID, + message: NotificationMessage.TEMPLATE_SAVE, + title: NotificationTitle.TEMPLATE_SAVED, + }); + }) + .catch(() => { + notificationService.error({ + id: TEMPLATE_SAVE_FAILED_NOTOFICATION_ID, + message: NotificationMessage.TEMPLATE_SAVE_FAILED, + title: NotificationTitle.TEMPLATE_SAVE_FAILED, + }); + }); + }, [dispatch]); + useEffect(() => { if (isVideoScriptsGenerationReady) { dispatch(studioActions.setVideoScriptToPending()); @@ -258,6 +288,9 @@ const Studio: React.FC = () => { Submit to render + + Save as template + diff --git a/frontend/src/bundles/studio/store/actions.ts b/frontend/src/bundles/studio/store/actions.ts index 05c79464b..9c9559a60 100644 --- a/frontend/src/bundles/studio/store/actions.ts +++ b/frontend/src/bundles/studio/store/actions.ts @@ -5,9 +5,11 @@ import { type UpdateVideoRequestDto } from 'shared'; import { type AsyncThunkConfig } from '~/bundles/common/types/types.js'; import { type AvatarGetAllResponseDto, + type CreateTemplateResponseDto, type CreateVideoRequestDto, type GenerateSpeechRequestDto, type GenerateSpeechResponseDto, + type GetTemplatesResponseDto, type GetVoicesResponseDto, type RenderAvatarResponseDto, type Script, @@ -131,11 +133,52 @@ const updateVideo = createAsyncThunk< return videosApi.updateVideo(payload, videoId as string); }); +const loadPublicTemplates = createAsyncThunk< + GetTemplatesResponseDto, + undefined, + AsyncThunkConfig +>(`${sliceName}/load-public-templates`, (_, { extra }) => { + const { templatesApi } = extra; + + return templatesApi.loadPublicTemplates(); +}); + +const loadUserTemplates = createAsyncThunk< + GetTemplatesResponseDto, + undefined, + AsyncThunkConfig +>(`${sliceName}/load-user-templates`, (_, { extra }) => { + const { templatesApi } = extra; + + return templatesApi.loadUserTemplates(); +}); + +const createTemplate = createAsyncThunk< + CreateTemplateResponseDto, + undefined, + AsyncThunkConfig +>(`${sliceName}/create-template`, (_, { extra, getState }) => { + const { templatesApi } = extra; + const { scripts, scenes, videoName, videoSize } = getState().studio; + + return templatesApi.createTemplate({ + composition: { + scenes, + scripts: getVoicesConfigs(scripts), + videoOrientation: videoSize, + }, + name: videoName, + }); +}); + export { + createTemplate, generateAllScriptsSpeech, generateScriptSpeech, generateScriptSpeechPreview, loadAvatars, + loadPublicTemplates, + loadUserTemplates, loadVoices, renderAvatar, saveVideo, diff --git a/frontend/src/bundles/studio/store/selectors.ts b/frontend/src/bundles/studio/store/selectors.ts index 751ca05e7..af87aaaa3 100644 --- a/frontend/src/bundles/studio/store/selectors.ts +++ b/frontend/src/bundles/studio/store/selectors.ts @@ -5,6 +5,7 @@ import { type RootState } from '~/bundles/common/types/types.js'; import { type Script, + type Template, type VideoGetAllItemResponseDto, } from '../types/types.js'; @@ -13,6 +14,12 @@ const selectScrips = (state: RootState): Script[] => state.studio.scripts; const selectVideos = (state: RootState): VideoGetAllItemResponseDto[] => state.home.videos; +const selectPublicTemplates = (state: RootState): Template[] => + state.studio.templates.public; + +const selectUserTemplates = (state: RootState): Template[] => + state.studio.templates.user; + const selectTotalDuration = createSelector([selectScrips], (scripts) => { const totalDuration = scripts.reduce( (total, script) => total + script.duration, @@ -29,4 +36,17 @@ const selectVideoDataById = createSelector( }, ); -export { selectTotalDuration, selectVideoDataById }; +const selectTemplateDataById = createSelector( + [selectPublicTemplates, selectUserTemplates, (_, id: string): string => id], + (publicTemplates, userTemplates, id) => { + const publicTemplate = publicTemplates.find( + (template) => template.id === id, + ); + if (!publicTemplate) { + return userTemplates.find((template) => template.id === id); + } + return publicTemplate; + }, +); + +export { selectTemplateDataById, selectTotalDuration, selectVideoDataById }; diff --git a/frontend/src/bundles/studio/store/slice.ts b/frontend/src/bundles/studio/store/slice.ts index c3b635479..710cd2016 100644 --- a/frontend/src/bundles/studio/store/slice.ts +++ b/frontend/src/bundles/studio/store/slice.ts @@ -40,15 +40,19 @@ import { type Scene, type SceneAvatar, type SelectedItem, + type Template, type TimelineItemWithSpan, type VideoGetAllItemResponseDto, type Voice, } from '../types/types.js'; import { + createTemplate, generateAllScriptsSpeech, generateScriptSpeech, generateScriptSpeechPreview, loadAvatars, + loadPublicTemplates, + loadUserTemplates, loadVoices, renderAvatar, saveVideo, @@ -93,6 +97,11 @@ type State = { isDraftSaved: boolean; videoId: string | null; voices: Voice[]; + templates: { + public: Template[] | []; + user: Template[] | []; + isUserLoaded: boolean; + }; ui: { destinationPointer: DestinationPointer | null; selectedItem: SelectedItem | null; @@ -119,6 +128,11 @@ const initialState: State = { isDraftSaved: true, videoId: null, voices: [], + templates: { + public: [], + user: [], + isUserLoaded: false, + }, ui: { destinationPointer: null, selectedItem: null, @@ -422,6 +436,7 @@ const { reducer, actions, name } = createSlice({ state.videoName = name; state.videoId = id; + state.videoSize = composition.videoOrientation; state.scenes = composition.scenes; state.scripts = composition.scripts.map( (script: CompositionScript) => { @@ -438,6 +453,27 @@ const { reducer, actions, name } = createSlice({ }, ); }, + loadTemplate(state, action: PayloadAction