Commit 3af09475 authored by luopl's avatar luopl
Browse files

"Initial commit"

parents
Pipeline #3140 canceled with stages
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import EffectsCarousel from '@/common/components/effects/EffectsCarousel';
import {backgroundEffects} from '@/common/components/effects/EffectsUtils';
import useVideoEffect from '@/common/components/video/editor/useVideoEffect';
import {
EffectIndex,
effectPresets,
} from '@/common/components/video/effects/Effects';
import {ListBoxes, MagicWand, MagicWandFilled} from '@carbon/icons-react';
import {useCallback, useRef, useState} from 'react';
import {Button} from 'react-daisyui';
import EffectsToolbarBottomActions from '@/common/components/effects/EffectsToolbarBottomActions';
import ToolbarProgressChip from '@/common/components/toolbar/ToolbarProgressChip';
import {
activeBackgroundEffectAtom,
activeHighlightEffectAtom,
activeHighlightEffectGroupAtom,
} from '@/demo/atoms';
import {BLUE_PINK_FILL} from '@/theme/gradientStyle';
import {useAtomValue} from 'jotai';
type Props = {
onTabChange: (newIndex: number) => void;
};
export default function MobileEffectsToolbar({onTabChange}: Props) {
const preset = useRef(0);
const setEffect = useVideoEffect();
const [showEffectsCarousels, setShowEffectsCarousels] = useState<boolean>();
const activeBackground = useAtomValue(activeBackgroundEffectAtom);
const activeHighlight = useAtomValue(activeHighlightEffectAtom);
const activeHighlightEffectsGroup = useAtomValue(
activeHighlightEffectGroupAtom,
);
const handleTogglePreset = useCallback(() => {
preset.current++;
const [background, highlight] =
effectPresets[preset.current % effectPresets.length];
setEffect(background.name, EffectIndex.BACKGROUND, {
variant: background.variant,
});
setEffect(highlight.name, EffectIndex.HIGHLIGHT, {
variant: highlight.variant,
});
}, [setEffect]);
return (
<div className="w-full">
{showEffectsCarousels ? (
<div className="flex gap-2 px-2 py-4 items-center p-6">
<Button
color="ghost"
className="mt-6 !px-2 !text-[#FB73A5]"
startIcon={<MagicWand size={20} />}
onClick={handleTogglePreset}
/>
<EffectsCarousel
label="Highlights"
effects={activeHighlightEffectsGroup}
activeEffect={activeHighlight.name}
index={1}
/>
<EffectsCarousel
label="Background"
effects={backgroundEffects}
activeEffect={activeBackground.name}
index={0}
/>
</div>
) : (
<div className="flex flex-col gap-6 p-6">
<div className="text-sm text-white">
<ToolbarProgressChip />
Apply visual effects to your selected objects and the background.
</div>
<div className="grid grid-cols-2 gap-2">
<Button
color="ghost"
endIcon={<MagicWandFilled size={20} />}
className={`font-bold bg-black !rounded-full !bg-gradient-to-br ${BLUE_PINK_FILL} border-none text-white`}
onClick={handleTogglePreset}>
Surprise Me
</Button>
<Button
color="ghost"
className={`font-bold bg-black !rounded-full border-none text-white`}
startIcon={<ListBoxes size={20} />}
onClick={() => setShowEffectsCarousels(true)}>
More effects
</Button>
</div>
</div>
)}
<EffectsToolbarBottomActions onTabChange={onTabChange} />
</div>
);
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {moreEffects} from '@/common/components/effects/EffectsUtils';
import EffectVariantBadge from '@/common/components/effects/EffectVariantBadge';
import ToolbarActionIcon from '@/common/components/toolbar/ToolbarActionIcon';
import ToolbarSection from '@/common/components/toolbar/ToolbarSection';
import useVideoEffect from '@/common/components/video/editor/useVideoEffect';
import {EffectIndex} from '@/common/components/video/effects/Effects';
import {activeHighlightEffectAtom} from '@/demo/atoms';
import {useAtomValue} from 'jotai';
export default function MoreFunEffects() {
const setEffect = useVideoEffect();
const activeEffect = useAtomValue(activeHighlightEffectAtom);
return (
<ToolbarSection title="Selected Objects" borderBottom={true}>
{moreEffects.map(effect => {
return (
<ToolbarActionIcon
variant="toggle"
key={effect.title}
icon={effect.Icon}
title={effect.title}
isActive={activeEffect.name === effect.effectName}
badge={
activeEffect.name === effect.effectName && (
<EffectVariantBadge
label={`${activeEffect.variant + 1}/${activeEffect.numVariants}`}
/>
)
}
onClick={() => {
setEffect(effect.effectName, EffectIndex.HIGHLIGHT);
}}
/>
);
})}
</ToolbarSection>
);
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type {VideoGalleryTriggerProps} from '@/common/components/gallery/DemoVideoGalleryModal';
import DemoVideoGalleryModal from '@/common/components/gallery/DemoVideoGalleryModal';
import useVideo from '@/common/components/video/editor/useVideo';
import Logger from '@/common/logger/Logger';
import {isStreamingAtom, uploadingStateAtom, VideoData} from '@/demo/atoms';
import {useAtomValue, useSetAtom} from 'jotai';
import {ComponentType, useCallback} from 'react';
import {useNavigate} from 'react-router-dom';
type Props = {
videoGalleryModalTrigger?: ComponentType<VideoGalleryTriggerProps>;
showUploadInGallery?: boolean;
onChangeVideo?: () => void;
};
export default function ChangeVideoModal({
videoGalleryModalTrigger: VideoGalleryModalTriggerComponent,
showUploadInGallery = true,
onChangeVideo,
}: Props) {
const isStreaming = useAtomValue(isStreamingAtom);
const setUploadingState = useSetAtom(uploadingStateAtom);
const video = useVideo();
const navigate = useNavigate();
const handlePause = useCallback(() => {
video?.pause();
}, [video]);
function handlePauseOrAbortVideo() {
if (isStreaming) {
video?.abortStreamMasks();
} else {
handlePause();
}
}
function handleSwitchVideos(video: VideoData) {
// Retain any search parameter
navigate(
{
pathname: location.pathname,
search: location.search,
},
{
state: {
video,
},
},
);
onChangeVideo?.();
}
function handleUploadVideoError(error: Error) {
setUploadingState('error');
Logger.error(error);
}
return (
<DemoVideoGalleryModal
trigger={VideoGalleryModalTriggerComponent}
showUploadInGallery={showUploadInGallery}
onOpen={handlePauseOrAbortVideo}
onSelect={handleSwitchVideos}
onUploadVideoError={handleUploadVideoError}
/>
);
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import ResponsiveButton from '@/common/components/button/ResponsiveButton';
import type {VideoGalleryTriggerProps} from '@/common/components/gallery/DemoVideoGalleryModal';
import {ImageCopy} from '@carbon/icons-react';
export default function DefaultVideoGalleryModalTrigger({
onClick,
}: VideoGalleryTriggerProps) {
return (
<ResponsiveButton
color="ghost"
className="hover:!bg-black"
startIcon={<ImageCopy size={20} />}
onClick={onClick}>
Change video
</ResponsiveButton>
);
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {DemoVideoGalleryQuery} from '@/common/components/gallery/__generated__/DemoVideoGalleryQuery.graphql';
import VideoGalleryUploadVideo from '@/common/components/gallery/VideoGalleryUploadPhoto';
import VideoPhoto from '@/common/components/gallery/VideoPhoto';
import useScreenSize from '@/common/screen/useScreenSize';
import {VideoData} from '@/demo/atoms';
import {DEMO_SHORT_NAME} from '@/demo/DemoConfig';
import {fontSize, fontWeight, spacing} from '@/theme/tokens.stylex';
import stylex from '@stylexjs/stylex';
import {useMemo} from 'react';
import PhotoAlbum, {Photo, RenderPhotoProps} from 'react-photo-album';
import {graphql, useLazyLoadQuery} from 'react-relay';
import {useLocation, useNavigate} from 'react-router-dom';
const styles = stylex.create({
container: {
display: 'flex',
flexDirection: 'column',
marginHorizontal: spacing[1],
height: '100%',
lineHeight: 1.2,
paddingTop: spacing[8],
},
headerContainer: {
marginBottom: spacing[8],
fontWeight: fontWeight['medium'],
fontSize: fontSize['2xl'],
'@media screen and (max-width: 768px)': {
marginTop: spacing[0],
marginBottom: spacing[8],
marginHorizontal: spacing[4],
fontSize: fontSize['xl'],
},
},
albumContainer: {
flex: '1 1 0%',
width: '100%',
overflowY: 'auto',
},
});
type Props = {
showUploadInGallery?: boolean;
onSelect?: (video: VideoPhotoData) => void;
onUpload: (video: VideoData) => void;
onUploadStart?: () => void;
onUploadError?: (error: Error) => void;
};
type VideoPhotoData = Photo &
VideoData & {
poster: string;
isUploadOption: boolean;
};
export default function DemoVideoGallery({
showUploadInGallery = false,
onSelect,
onUpload,
onUploadStart,
onUploadError,
}: Props) {
const navigate = useNavigate();
const location = useLocation();
const {isMobile: isMobileScreenSize} = useScreenSize();
const data = useLazyLoadQuery<DemoVideoGalleryQuery>(
graphql`
query DemoVideoGalleryQuery {
videos {
edges {
node {
id
path
posterPath
url
posterUrl
height
width
posterUrl
}
}
}
}
`,
{},
);
const allVideos: VideoPhotoData[] = useMemo(() => {
return data.videos.edges.map(video => {
return {
src: video.node.url,
path: video.node.path,
poster: video.node.posterPath,
posterPath: video.node.posterPath,
url: video.node.url,
posterUrl: video.node.posterUrl,
width: video.node.width,
height: video.node.height,
isUploadOption: false,
} as VideoPhotoData;
});
}, [data.videos.edges]);
const shareableVideos: VideoPhotoData[] = useMemo(() => {
const filteredVideos = [...allVideos];
if (showUploadInGallery) {
const uploadOption = {
src: '',
width: 1280,
height: 720,
poster: '',
isUploadOption: true,
} as VideoPhotoData;
filteredVideos.unshift(uploadOption);
}
return filteredVideos;
}, [allVideos, showUploadInGallery]);
const renderPhoto = ({
photo: video,
imageProps,
}: RenderPhotoProps<VideoPhotoData>) => {
const {style} = imageProps;
const {url, posterUrl} = video;
return video.isUploadOption ? (
<VideoGalleryUploadVideo
style={style}
onUpload={handleUploadVideo}
onUploadError={onUploadError}
onUploadStart={onUploadStart}
/>
) : (
<VideoPhoto
src={url}
poster={posterUrl}
style={style}
onClick={() => {
navigate(location.pathname, {
state: {
video,
},
});
onSelect?.(video);
}}
/>
);
};
function handleUploadVideo(video: VideoData) {
navigate(location.pathname, {
state: {
video,
},
});
onUpload?.(video);
}
const descriptionStyle = 'text-sm md:text-base text-gray-400 leading-snug';
return (
<div {...stylex.props(styles.container)}>
<div {...stylex.props(styles.albumContainer)}>
<div className="pt-0 md:px-16 md:pt-8 md:pb-8">
<div {...stylex.props(styles.headerContainer)}>
<h3 className="mb-2">
Select a video to try{' '}
<span className="hidden md:inline">
with the {DEMO_SHORT_NAME}
</span>
</h3>
<p className={descriptionStyle}>
You’ll be able to download what you make.
</p>
</div>
<PhotoAlbum<VideoPhotoData>
layout="rows"
photos={shareableVideos}
targetRowHeight={isMobileScreenSize ? 120 : 200}
rowConstraints={{
singleRowMaxHeight: isMobileScreenSize ? 120 : 240,
maxPhotos: 3,
}}
renderPhoto={renderPhoto}
spacing={4}
/>
</div>
</div>
</div>
);
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import DefaultVideoGalleryModalTrigger from '@/common/components/gallery/DefaultVideoGalleryModalTrigger';
import {
frameIndexAtom,
sessionAtom,
uploadingStateAtom,
VideoData,
} from '@/demo/atoms';
import {spacing} from '@/theme/tokens.stylex';
import {Close} from '@carbon/icons-react';
import stylex from '@stylexjs/stylex';
import {useSetAtom} from 'jotai';
import {ComponentType, useCallback, useRef} from 'react';
import {Modal} from 'react-daisyui';
import DemoVideoGallery from './DemoVideoGallery';
const styles = stylex.create({
container: {
position: 'relative',
minWidth: '85vw',
minHeight: '85vh',
overflow: 'hidden',
color: '#fff',
boxShadow: '0 0 100px 50px #000',
borderRadius: 16,
border: '2px solid transparent',
background:
'linear-gradient(#1A1C1F, #1A1C1F) padding-box, linear-gradient(to right bottom, #FB73A5,#595FEF,#94EAE2,#FCCB6B) border-box',
},
closeButton: {
position: 'absolute',
top: 0,
right: 0,
padding: spacing[3],
zIndex: 10,
cursor: 'pointer',
':hover': {
opacity: 0.7,
},
},
galleryContainer: {
position: 'absolute',
top: spacing[4],
left: 0,
right: 0,
bottom: 0,
overflowY: 'auto',
},
});
export type VideoGalleryTriggerProps = {
onClick: () => void;
};
type Props = {
trigger?: ComponentType<VideoGalleryTriggerProps>;
showUploadInGallery?: boolean;
onOpen?: () => void;
onSelect?: (video: VideoData, isUpload?: boolean) => void;
onUploadVideoError?: (error: Error) => void;
};
export default function DemoVideoGalleryModal({
trigger: VideoGalleryModalTrigger = DefaultVideoGalleryModalTrigger,
showUploadInGallery = false,
onOpen,
onSelect,
onUploadVideoError,
}: Props) {
const modalRef = useRef<HTMLDialogElement | null>(null);
const setFrameIndex = useSetAtom(frameIndexAtom);
const setUploadingState = useSetAtom(uploadingStateAtom);
const setSession = useSetAtom(sessionAtom);
function openModal() {
const modal = modalRef.current;
if (modal != null) {
modal.style.display = 'grid';
modal.showModal();
}
}
function closeModal() {
const modal = modalRef.current;
if (modal != null) {
modal.close();
modal.style.display = 'none';
}
}
const handleSelect = useCallback(
async (video: VideoData, isUpload?: boolean) => {
closeModal();
setFrameIndex(0);
onSelect?.(video, isUpload);
setUploadingState('default');
setSession(null);
},
[setFrameIndex, onSelect, setUploadingState, setSession],
);
function handleUploadVideoStart() {
setUploadingState('uploading');
closeModal();
}
function handleOpenVideoGalleryModal() {
onOpen?.();
openModal();
}
return (
<>
<VideoGalleryModalTrigger onClick={handleOpenVideoGalleryModal} />
<Modal ref={modalRef} {...stylex.props(styles.container)}>
<div onClick={closeModal} {...stylex.props(styles.closeButton)}>
<Close size={28} />
</div>
<Modal.Body>
<div {...stylex.props(styles.galleryContainer)}>
<DemoVideoGallery
showUploadInGallery={showUploadInGallery}
onSelect={video => handleSelect(video)}
onUpload={video => handleSelect(video, true)}
onUploadStart={handleUploadVideoStart}
onUploadError={onUploadVideoError}
/>
</div>
</Modal.Body>
</Modal>
</>
);
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import useUploadVideo from '@/common/components/gallery/useUploadVideo';
import useScreenSize from '@/common/screen/useScreenSize';
import {VideoData} from '@/demo/atoms';
import {MAX_UPLOAD_FILE_SIZE} from '@/demo/DemoConfig';
import {BLUE_PINK_FILL_BR} from '@/theme/gradientStyle';
import {RetryFailed, Upload} from '@carbon/icons-react';
import {CSSProperties, ReactNode} from 'react';
import {Loading} from 'react-daisyui';
type Props = {
style: CSSProperties;
onUpload: (video: VideoData) => void;
onUploadStart?: () => void;
onUploadError?: (error: Error) => void;
};
export default function VideoGalleryUploadVideo({
style,
onUpload,
onUploadStart,
onUploadError,
}: Props) {
const {getRootProps, getInputProps, isUploading, error} = useUploadVideo({
onUpload,
onUploadStart,
onUploadError,
});
const {isMobile} = useScreenSize();
return (
<div className={`cursor-pointer ${BLUE_PINK_FILL_BR}`} style={style}>
<span {...getRootProps()}>
<input {...getInputProps()} />
<div className="relative w-full h-full">
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
{isUploading && (
<IconWrapper
icon={
<Loading
size={isMobile ? 'md' : 'lg'}
className="text-white"
/>
}
title="Uploading ..."
/>
)}
{error !== null && (
<IconWrapper
icon={<RetryFailed color="white" size={isMobile ? 24 : 32} />}
title={error}
/>
)}
{!isUploading && error === null && (
<IconWrapper
icon={<Upload color="white" size={isMobile ? 24 : 32} />}
title={
<>
Upload{' '}
<div className="text-xs opacity-70">
Max {MAX_UPLOAD_FILE_SIZE}
</div>
</>
}
/>
)}
</div>
</div>
</span>
</div>
);
}
type IconWrapperProps = {
icon: ReactNode;
title: ReactNode | string;
};
function IconWrapper({icon, title}: IconWrapperProps) {
return (
<>
<div className="flex justify-center">{icon}</div>
<div className="mt-1 text-sm md:text-lg text-white font-medium text-center leading-tight">
{title}
</div>
</>
);
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Logger from '@/common/logger/Logger';
import stylex from '@stylexjs/stylex';
import {
CSSProperties,
MouseEventHandler,
useCallback,
useEffect,
useRef,
} from 'react';
const styles = stylex.create({
background: {
backgroundRepeat: 'no-repeat',
backgroundSize: 'cover',
backgroundPosition: 'center',
cursor: 'pointer',
},
video: {
width: '100%',
height: '100%',
},
});
type Props = {
onClick: MouseEventHandler<HTMLVideoElement> | undefined;
src: string;
poster: string;
style: CSSProperties;
};
export default function VideoPhoto({src, poster, style, onClick}: Props) {
const videoRef = useRef<HTMLVideoElement | null>(null);
const playPromiseRef = useRef<Promise<void> | null>(null);
const play = useCallback(() => {
const video = videoRef.current;
// Only play video if it is not already playing
if (video != null && video.paused) {
// This quirky way of handling video play/pause in the browser is needed
// due to the async nature of the video play API:
// https://developer.chrome.com/blog/play-request-was-interrupted/
const playPromise = video.play();
playPromise.catch(error => {
Logger.error('Failed to play video', error);
});
playPromiseRef.current = playPromise;
}
}, []);
const pause = useCallback(() => {
// Only pause video if it is playing
const playPromise = playPromiseRef.current;
if (playPromise != null) {
playPromise
.then(() => {
videoRef.current?.pause();
})
.catch(error => {
Logger.error('Failed to pause video', error);
})
.finally(() => {
playPromiseRef.current = null;
});
}
}, []);
useEffect(() => {
return () => {
pause();
};
}, [pause]);
return (
<div
style={{
...style,
backgroundImage: `url(${poster})`,
}}
{...stylex.props(styles.background)}>
<video
ref={videoRef}
{...stylex.props(styles.video)}
preload="none"
playsInline
loop
muted
title="Gallery Video"
poster={poster}
onMouseEnter={play}
onMouseLeave={pause}
onClick={onClick}>
<source src={src} type="video/mp4" />
Sorry, your browser does not support embedded videos.
</video>
</div>
);
}
/**
* @generated SignedSource<<db7e183e1996cf656749b4e33c2424e6>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest, Query } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type DemoVideoGalleryModalQuery$variables = Record<PropertyKey, never>;
export type DemoVideoGalleryModalQuery$data = {
readonly " $fragmentSpreads": FragmentRefs<"DatasetsDropdown_datasets" | "VideoGallery_videos">;
};
export type DemoVideoGalleryModalQuery = {
response: DemoVideoGalleryModalQuery$data;
variables: DemoVideoGalleryModalQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
}
],
v1 = [
{
"kind": "Literal",
"name": "after",
"value": ""
},
{
"kind": "Literal",
"name": "first",
"value": 20
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "DemoVideoGalleryModalQuery",
"selections": [
{
"args": null,
"kind": "FragmentSpread",
"name": "DatasetsDropdown_datasets"
},
{
"args": null,
"kind": "FragmentSpread",
"name": "VideoGallery_videos"
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [],
"kind": "Operation",
"name": "DemoVideoGalleryModalQuery",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "DatasetConnection",
"kind": "LinkedField",
"name": "datasets",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "DatasetEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Dataset",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": (v0/*: any*/),
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": "VideoConnection",
"kind": "LinkedField",
"name": "videos",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasPreviousPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "startCursor",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "endCursor",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "VideoEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "Video",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "path",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "posterPath",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "url",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "posterUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "width",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "height",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "Dataset",
"kind": "LinkedField",
"name": "dataset",
"plural": false,
"selections": (v0/*: any*/),
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "VideoPermissions",
"kind": "LinkedField",
"name": "permissions",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "canShare",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "canDownload",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "videos(after:\"\",first:20)"
},
{
"alias": null,
"args": (v1/*: any*/),
"filters": [
"datasetName"
],
"handle": "connection",
"key": "VideoGallery_videos",
"kind": "LinkedHandle",
"name": "videos"
}
]
},
"params": {
"cacheID": "e0bccf553377682e6bc283c2ce53bee5",
"id": null,
"metadata": {},
"name": "DemoVideoGalleryModalQuery",
"operationKind": "query",
"text": "query DemoVideoGalleryModalQuery {\n ...DatasetsDropdown_datasets\n ...VideoGallery_videos\n}\n\nfragment DatasetsDropdown_datasets on Query {\n datasets {\n edges {\n node {\n name\n }\n }\n }\n}\n\nfragment VideoGallery_videos on Query {\n videos(first: 20, after: \"\") {\n __typename\n pageInfo {\n __typename\n hasPreviousPage\n hasNextPage\n startCursor\n endCursor\n }\n edges {\n __typename\n node {\n __typename\n id\n path\n posterPath\n url\n posterUrl\n width\n height\n dataset {\n name\n }\n permissions {\n canShare\n canDownload\n }\n }\n cursor\n }\n }\n}\n"
}
};
})();
(node as any).hash = "d09e34e2b9f2e25c2d564106de5f9c89";
export default node;
/**
* @generated SignedSource<<20d31a82b5f3b251b0e42b4f0e3522b8>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest, Query } from 'relay-runtime';
export type DemoVideoGalleryQuery$variables = Record<PropertyKey, never>;
export type DemoVideoGalleryQuery$data = {
readonly videos: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly height: number;
readonly id: any;
readonly path: string;
readonly posterPath: string | null | undefined;
readonly posterUrl: string;
readonly url: string;
readonly width: number;
};
}>;
};
};
export type DemoVideoGalleryQuery = {
response: DemoVideoGalleryQuery$data;
variables: DemoVideoGalleryQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"alias": null,
"args": null,
"concreteType": "VideoConnection",
"kind": "LinkedField",
"name": "videos",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "VideoEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Video",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "path",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "posterPath",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "url",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "posterUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "height",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "width",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "DemoVideoGalleryQuery",
"selections": (v0/*: any*/),
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [],
"kind": "Operation",
"name": "DemoVideoGalleryQuery",
"selections": (v0/*: any*/)
},
"params": {
"cacheID": "4dae74153a5528f2631b59dfb0adb021",
"id": null,
"metadata": {},
"name": "DemoVideoGalleryQuery",
"operationKind": "query",
"text": "query DemoVideoGalleryQuery {\n videos {\n edges {\n node {\n id\n path\n posterPath\n url\n posterUrl\n height\n width\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "d22ac5e58f6e4eb696651be49b410e4e";
export default node;
/**
* @generated SignedSource<<76014dced98d6c8989e7322712e38963>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest, Mutation } from 'relay-runtime';
export type useUploadVideoMutation$variables = {
file: any;
};
export type useUploadVideoMutation$data = {
readonly uploadVideo: {
readonly height: number;
readonly id: any;
readonly path: string;
readonly posterPath: string | null | undefined;
readonly posterUrl: string;
readonly url: string;
readonly width: number;
};
};
export type useUploadVideoMutation = {
response: useUploadVideoMutation$data;
variables: useUploadVideoMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "file"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "file",
"variableName": "file"
}
],
"concreteType": "Video",
"kind": "LinkedField",
"name": "uploadVideo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "height",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "width",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "url",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "path",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "posterPath",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "posterUrl",
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "useUploadVideoMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "useUploadVideoMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "dcbaf1bf411627fdb9dfbb827592cfc0",
"id": null,
"metadata": {},
"name": "useUploadVideoMutation",
"operationKind": "mutation",
"text": "mutation useUploadVideoMutation(\n $file: Upload!\n) {\n uploadVideo(file: $file) {\n id\n height\n width\n url\n path\n posterPath\n posterUrl\n }\n}\n"
}
};
})();
(node as any).hash = "710e462504d76597af8695b7fc70b4cf";
export default node;
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {useUploadVideoMutation} from '@/common/components/gallery/__generated__/useUploadVideoMutation.graphql';
import Logger from '@/common/logger/Logger';
import {VideoData} from '@/demo/atoms';
import {useState} from 'react';
import {FileRejection, FileWithPath, useDropzone} from 'react-dropzone';
import {graphql, useMutation} from 'react-relay';
const ACCEPT_VIDEOS = {
'video/mp4': ['.mp4'],
'video/quicktime': ['.mov'],
};
// 70 MB default max video upload size
const MAX_FILE_SIZE_IN_MB = 70;
const MAX_VIDEO_UPLOAD_SIZE = MAX_FILE_SIZE_IN_MB * 1024 ** 2;
type Props = {
onUpload: (video: VideoData) => void;
onUploadStart?: () => void;
onUploadError?: (error: Error) => void;
};
export default function useUploadVideo({
onUpload,
onUploadStart,
onUploadError,
}: Props) {
const [error, setError] = useState<string | null>(null);
const [commit, isMutationInFlight] = useMutation<useUploadVideoMutation>(
graphql`
mutation useUploadVideoMutation($file: Upload!) {
uploadVideo(file: $file) {
id
height
width
url
path
posterPath
posterUrl
}
}
`,
);
const {getRootProps, getInputProps} = useDropzone({
accept: ACCEPT_VIDEOS,
multiple: false,
maxFiles: 1,
onDrop: (
acceptedFiles: FileWithPath[],
fileRejections: FileRejection[],
) => {
setError(null);
// Check if any of the files (only 1 file allowed) is rejected. The
// rejected file has an error (e.g., 'file-too-large'). Rendering an
// appropriate message.
if (fileRejections.length > 0 && fileRejections[0].errors.length > 0) {
const code = fileRejections[0].errors[0].code;
if (code === 'file-too-large') {
setError(
`File too large. Try a video under ${MAX_FILE_SIZE_IN_MB} MB`,
);
return;
}
}
if (acceptedFiles.length === 0) {
setError('File not accepted. Please try again.');
return;
}
if (acceptedFiles.length > 1) {
setError('Too many files. Please try again with 1 file.');
return;
}
onUploadStart?.();
const file = acceptedFiles[0];
commit({
variables: {
file,
},
uploadables: {
file,
},
onCompleted: response => onUpload(response.uploadVideo),
onError: error => {
Logger.error(error);
onUploadError?.(error);
setError('Upload failed.');
},
});
},
onError: error => {
Logger.error(error);
setError('File not supported.');
},
maxSize: MAX_VIDEO_UPLOAD_SIZE,
});
return {
getRootProps,
getInputProps,
isUploading: isMutationInFlight,
error,
setError,
};
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
type Props = {
className?: string;
};
export function GitHubIcon({className}: Props) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" className={className}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12 2C6.477 2 2 6.463 2 11.97c0 4.404 2.865 8.14 6.839 9.458.5.092.682-.216.682-.48 0-.236-.008-.864-.013-1.695-2.782.602-3.369-1.337-3.369-1.337-.454-1.151-1.11-1.458-1.11-1.458-.908-.618.069-.606.069-.606 1.003.07 1.531 1.027 1.531 1.027.892 1.524 2.341 1.084 2.91.828.092-.643.35-1.083.636-1.332-2.22-.251-4.555-1.107-4.555-4.927 0-1.088.39-1.979 1.029-2.675-.103-.252-.446-1.266.098-2.638 0 0 .84-.268 2.75 1.022A9.607 9.607 0 0 1 12 6.82c.85.004 1.705.114 2.504.336 1.909-1.29 2.747-1.022 2.747-1.022.546 1.372.202 2.386.1 2.638.64.696 1.028 1.587 1.028 2.675 0 3.83-2.339 4.673-4.566 4.92.359.307.678.915.678 1.846 0 1.332-.012 2.407-.012 2.734 0 .267.18.577.688.48 3.97-1.32 6.833-5.054 6.833-9.458C22 6.463 17.522 2 12 2Z"></path>
</svg>
);
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Package} from '@carbon/icons-react';
import OptionButton from './OptionButton';
import useDownloadVideo from './useDownloadVideo';
export default function DownloadOption() {
const {download, state} = useDownloadVideo();
return (
<OptionButton
title="Download"
Icon={Package}
loadingProps={{
loading: state === 'started' || state === 'encoding',
label: 'Downloading...',
}}
onClick={download}
/>
);
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import ChangeVideoModal from '@/common/components/gallery/ChangeVideoModal';
import type {VideoGalleryTriggerProps} from '@/common/components/gallery/DemoVideoGalleryModal';
import useScreenSize from '@/common/screen/useScreenSize';
import {ImageCopy} from '@carbon/icons-react';
import OptionButton from './OptionButton';
type Props = {
onChangeVideo: () => void;
};
export default function GalleryOption({onChangeVideo}: Props) {
return (
<ChangeVideoModal
videoGalleryModalTrigger={GalleryTrigger}
showUploadInGallery={false}
onChangeVideo={onChangeVideo}
/>
);
}
function GalleryTrigger({onClick}: VideoGalleryTriggerProps) {
const {isMobile} = useScreenSize();
return (
<OptionButton
variant="flat"
title={isMobile ? 'Gallery' : 'Browse gallery'}
Icon={ImageCopy}
onClick={onClick}
/>
);
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import MoreOptionsToolbarBottomActions from '@/common/components/options/MoreOptionsToolbarBottomActions';
import ShareSection from '@/common/components/options/ShareSection';
import TryAnotherVideoSection from '@/common/components/options/TryAnotherVideoSection';
import useMessagesSnackbar from '@/common/components/snackbar/useDemoMessagesSnackbar';
import ToolbarHeaderWrapper from '@/common/components/toolbar/ToolbarHeaderWrapper';
import useScreenSize from '@/common/screen/useScreenSize';
import {useEffect, useRef} from 'react';
type Props = {
onTabChange: (newIndex: number) => void;
};
export default function MoreOptionsToolbar({onTabChange}: Props) {
const {isMobile} = useScreenSize();
const {clearMessage} = useMessagesSnackbar();
const didClearMessageSnackbar = useRef(false);
useEffect(() => {
if (!didClearMessageSnackbar.current) {
didClearMessageSnackbar.current = true;
clearMessage();
}
}, [clearMessage]);
return (
<div className="flex flex-col h-full">
<div className="grow">
<ToolbarHeaderWrapper
title="Nice work! What's next?"
className="pb-0 !border-b-0 !text-white"
showProgressChip={false}
/>
<ShareSection />
{!isMobile && <div className="h-[1px] bg-black mt-4 mb-8"></div>}
<TryAnotherVideoSection onTabChange={onTabChange} />
</div>
{!isMobile && (
<MoreOptionsToolbarBottomActions onTabChange={onTabChange} />
)}
</div>
);
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import RestartSessionButton from '@/common/components/session/RestartSessionButton';
import {
EFFECT_TOOLBAR_INDEX,
OBJECT_TOOLBAR_INDEX,
} from '@/common/components/toolbar/ToolbarConfig';
import {ChevronLeft} from '@carbon/icons-react';
import {Button} from 'react-daisyui';
import ToolbarBottomActionsWrapper from '../toolbar/ToolbarBottomActionsWrapper';
type Props = {
onTabChange: (newIndex: number) => void;
};
export default function MoreOptionsToolbarBottomActions({onTabChange}: Props) {
function handleReturnToEffectsTab() {
onTabChange(EFFECT_TOOLBAR_INDEX);
}
return (
<ToolbarBottomActionsWrapper>
<Button
color="ghost"
onClick={handleReturnToEffectsTab}
className="!px-4 !rounded-full font-medium text-white hover:bg-black"
startIcon={<ChevronLeft />}>
Edit effects
</Button>
<RestartSessionButton
onRestartSession={() => onTabChange(OBJECT_TOOLBAR_INDEX)}
/>
</ToolbarBottomActionsWrapper>
);
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import GradientBorder from '@/common/components/button/GradientBorder';
import useScreenSize from '@/common/screen/useScreenSize';
import {BLUE_PINK_FILL_BR} from '@/theme/gradientStyle';
import type {CarbonIconType} from '@carbon/icons-react';
import {Loading} from 'react-daisyui';
type Props = {
variant?: 'default' | 'flat' | 'gradient';
title: string | React.ReactNode;
Icon: CarbonIconType;
isActive?: boolean;
isDisabled?: boolean;
loadingProps?: {
loading: boolean;
label?: string;
};
onClick: () => void;
};
export default function OptionButton({
variant = 'default',
title,
Icon,
isActive = false,
isDisabled = false,
loadingProps,
onClick,
}: Props) {
const {isMobile} = useScreenSize();
const isLoading = loadingProps?.loading === true;
function handleClick() {
if (isDisabled) {
return;
}
onClick();
}
const ButtonBase = (
<div
onClick={handleClick}
className={`relative rounded-lg h-full flex items-center justify-center
${variant === 'default' ? 'bg-graydark-700' : ''}
${!isDisabled && 'cursor-pointer'}
${isDisabled ? 'text-gray-300' : ''}
${isActive && BLUE_PINK_FILL_BR}`}>
<div className="flex gap-2 items-center py-4 md:py-6">
{isLoading ? (
<Loading size="md" className="mx-auto mt-1" />
) : (
<Icon
size={isMobile ? 24 : 28}
className={`mx-auto ${isDisabled ? 'text-gray-300' : 'text-white'}`}
/>
)}
<div className="text-base font-medium text-white">
{isLoading && loadingProps?.label != null
? loadingProps.label
: title}
</div>
</div>
</div>
);
return variant === 'gradient' ? (
<GradientBorder rounded={false} className={'rounded-lg md:rounded-full'}>
{ButtonBase}
</GradientBorder>
) : (
ButtonBase
);
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import DownloadOption from './DownloadOption';
export default function ShareSection() {
return (
<div className="p-5 md:p-8">
<DownloadOption />
</div>
);
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export async function handleSaveVideo(
videoPath: string,
fileName?: string,
): Promise<void> {
const blob = await fetch(videoPath).then(res => res.blob());
return new Promise(resolve => {
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.addEventListener('load', () => {
const elem = document.createElement('a');
elem.download = fileName ?? getFileName();
if (typeof reader.result === 'string') {
elem.href = reader.result;
}
elem.click();
resolve();
});
});
}
export function getFileName() {
const date = new Date();
const timestamp = date.getTime();
return `sam2_masked_video_${timestamp}.mp4`;
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment