Commit 75d93027 authored by bailuo's avatar bailuo
Browse files

init

parents
Pipeline #2479 failed with stages
in 0 seconds
/**
* 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 {PropsWithChildren} from 'react';
type Props = PropsWithChildren<{
className?: string;
message: string;
position?: 'left' | 'top' | 'right' | 'bottom';
}>;
/**
* This is a custom Tooltip component because React Daisy UI does not have an
* option to *only* show tooltip on large devices.
*/
export default function Tooltip({
children,
className = '',
message,
position = 'top',
}: Props) {
return (
<div
className={`lg:tooltip tooltip-${position} ${className}`}
data-tip={message}>
{children}
</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 useMessagesSnackbar from '@/common/components/snackbar/useDemoMessagesSnackbar';
import useVideo from '@/common/components/video/editor/useVideo';
import {activeTrackletObjectIdAtom, labelTypeAtom} from '@/demo/atoms';
import {Add} from '@carbon/icons-react';
import {useSetAtom} from 'jotai';
export default function AddObjectButton() {
const video = useVideo();
const setActiveTrackletId = useSetAtom(activeTrackletObjectIdAtom);
const setLabelType = useSetAtom(labelTypeAtom);
const {enqueueMessage} = useMessagesSnackbar();
async function addObject() {
enqueueMessage('addObjectClick');
const tracklet = await video?.createTracklet();
if (tracklet != null) {
setActiveTrackletId(tracklet.id);
setLabelType('positive');
}
}
return (
<div
onClick={addObject}
className="group flex justify-start mx-4 px-4 bg-transparent text-white !rounded-xl border-none cursor-pointer">
<div className="flex gap-6 items-center">
<div className=" group-hover:bg-graydark-700 border border-white relative h-12 w-12 md:w-20 md:h-20 shrink-0 rounded-lg flex items-center justify-center">
<Add size={36} className="group-hover:text-white text-gray-300" />
</div>
<div className="font-medium text-base">Add another object</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 useRestartSession from '@/common/components/session/useRestartSession';
import useMessagesSnackbar from '@/common/components/snackbar/useDemoMessagesSnackbar';
import useVideo from '@/common/components/video/editor/useVideo';
import {isPlayingAtom, isStreamingAtom, labelTypeAtom} from '@/demo/atoms';
import {Reset} from '@carbon/icons-react';
import stylex from '@stylexjs/stylex';
import {useAtomValue, useSetAtom} from 'jotai';
import {useState} from 'react';
import {Button, Loading} from 'react-daisyui';
const styles = stylex.create({
container: {
display: 'flex',
alignItems: 'center',
},
});
type Props = {
onRestart: () => void;
};
export default function ClearAllPointsInVideoButton({onRestart}: Props) {
const [isLoading, setIsLoading] = useState<boolean>(false);
const isPlaying = useAtomValue(isPlayingAtom);
const isStreaming = useAtomValue(isStreamingAtom);
const setLabelType = useSetAtom(labelTypeAtom);
const {clearMessage} = useMessagesSnackbar();
const {restartSession} = useRestartSession();
const video = useVideo();
async function handleRestart() {
if (video === null) {
return;
}
setIsLoading(true);
if (isPlaying) {
video.pause();
}
if (isStreaming) {
await video.abortStreamMasks();
}
const isSuccessful = await video.clearPointsInVideo();
if (!isSuccessful) {
await restartSession();
}
video.frame = 0;
setLabelType('positive');
onRestart();
clearMessage();
setIsLoading(false);
}
return (
<div {...stylex.props(styles.container)}>
<Button
color="ghost"
onClick={handleRestart}
className="!px-4 !rounded-full font-medium text-white hover:bg-black"
startIcon={isLoading ? <Loading size="sm" /> : <Reset size={20} />}>
Start over
</Button>
</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 PrimaryCTAButton from '@/common/components/button/PrimaryCTAButton';
import useVideo from '@/common/components/video/editor/useVideo';
import {ChevronRight} from '@carbon/icons-react';
type Props = {
onSessionClose: () => void;
};
export default function CloseSessionButton({onSessionClose}: Props) {
const video = useVideo();
function handleCloseSession() {
video?.closeSession();
video?.logAnnotations();
onSessionClose();
}
return (
<PrimaryCTAButton onClick={handleCloseSession} endIcon={<ChevronRight />}>
Good to go
</PrimaryCTAButton>
);
}
/**
* 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 ChangeVideo from '@/common/components/gallery/ChangeVideoModal';
import useMessagesSnackbar from '@/common/components/snackbar/useDemoMessagesSnackbar';
import {DEMO_SHORT_NAME} from '@/demo/DemoConfig';
import {useEffect, useRef} from 'react';
export default function FirstClickView() {
const isFirstClickMessageShown = useRef(false);
const {enqueueMessage} = useMessagesSnackbar();
useEffect(() => {
if (!isFirstClickMessageShown.current) {
isFirstClickMessageShown.current = true;
enqueueMessage('firstClick');
}
}, [enqueueMessage]);
return (
<div className="w-full h-full flex flex-col p-8">
<div className="grow flex flex-col gap-6">
<h2 className="text-2xl">Click an object in the video to start</h2>
<p className="!text-gray-60">
You&apos;ll be able to use {DEMO_SHORT_NAME} to make fun edits to any
video by tracking objects and applying visual effects.
</p>
<p className="!text-gray-60">
To start, click any object in the video.
</p>
</div>
<div className="flex items-center">
<ChangeVideo />
</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 {InformationFilled} from '@carbon/icons-react';
export default function LimitNotice() {
return (
<div className="mt-6 gap-3 mx-6 flex items-center text-gray-400">
<div>
<InformationFilled size={32} />
</div>
<div className="text-sm leading-snug">
In this demo, you can track up to 3 objects, even though the SAM 2 model
does not have a limit.
</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 ClearAllPointsInVideoButton from '@/common/components/annotations/ClearAllPointsInVideoButton';
import ObjectThumbnail from '@/common/components/annotations/ObjectThumbnail';
import {OBJECT_TOOLBAR_INDEX} from '@/common/components/toolbar/ToolbarConfig';
import {BaseTracklet} from '@/common/tracker/Tracker';
import {activeTrackletObjectIdAtom, trackletObjectsAtom} from '@/demo/atoms';
import {spacing} from '@/theme/tokens.stylex';
import stylex from '@stylexjs/stylex';
import {useAtomValue, useSetAtom} from 'jotai';
const styles = stylex.create({
container: {
display: 'flex',
padding: spacing[5],
borderTop: '1px solid #DEE3E9',
},
trackletsContainer: {
flexGrow: 1,
display: 'flex',
alignItems: 'center',
gap: spacing[5],
},
});
type Props = {
showActiveObject: () => void;
onTabChange: (newIndex: number) => void;
};
export default function MobileObjectsList({
showActiveObject,
onTabChange,
}: Props) {
const tracklets = useAtomValue(trackletObjectsAtom);
const setActiveTrackletId = useSetAtom(activeTrackletObjectIdAtom);
function handleSelectObject(tracklet: BaseTracklet) {
setActiveTrackletId(tracklet.id);
showActiveObject();
}
return (
<div {...stylex.props(styles.container)}>
<div {...stylex.props(styles.trackletsContainer)}>
{tracklets.map(tracklet => {
const {id, color, thumbnail} = tracklet;
return (
<ObjectThumbnail
key={id}
color={color}
thumbnail={thumbnail}
onClick={() => {
handleSelectObject(tracklet);
}}
/>
);
})}
</div>
<ClearAllPointsInVideoButton
onRestart={() => onTabChange(OBJECT_TOOLBAR_INDEX)}
/>
</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 MobileObjectsToolbarHeader from '@/common/components/annotations/MobileObjectsToolbarHeader';
import ObjectsToolbarBottomActions from '@/common/components/annotations/ObjectsToolbarBottomActions';
import {getObjectLabel} from '@/common/components/annotations/ObjectUtils';
import ToolbarObject from '@/common/components/annotations/ToolbarObject';
import MobileFirstClickBanner from '@/common/components/MobileFirstClickBanner';
import {activeTrackletObjectAtom, isFirstClickMadeAtom} from '@/demo/atoms';
import {useAtomValue} from 'jotai';
type Props = {
onTabChange: (newIndex: number) => void;
};
export default function MobileObjectsToolbar({onTabChange}: Props) {
const activeTracklet = useAtomValue(activeTrackletObjectAtom);
const isFirstClickMade = useAtomValue(isFirstClickMadeAtom);
if (!isFirstClickMade) {
return <MobileFirstClickBanner />;
}
return (
<div className="w-full">
<MobileObjectsToolbarHeader />
{activeTracklet != null && (
<ToolbarObject
label={getObjectLabel(activeTracklet)}
tracklet={activeTracklet}
isActive={true}
isMobile={true}
/>
)}
<ObjectsToolbarBottomActions 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 ToolbarProgressChip from '@/common/components/toolbar/ToolbarProgressChip';
import {isStreamingAtom, streamingStateAtom} from '@/demo/atoms';
import {useAtomValue} from 'jotai';
export default function MobileObjectsToolbarHeader() {
const isStreaming = useAtomValue(isStreamingAtom);
const streamingState = useAtomValue(streamingStateAtom);
return (
<div className="w-full flex gap-4 items-center px-5 py-5">
<div className="grow text-sm text-white">
<ToolbarProgressChip />
{streamingState === 'full'
? 'Review your selected objects across the video, and continue to edit if needed. Once everything looks good, press “Next” to continue.'
: isStreaming
? 'Watch the video closely for any places where your objects aren’t tracked correctly. You can also stop tracking to make additional edits.'
: 'Edit your object selection with a few more clicks if needed. Press “Track objects” to track your objects throughout the video.'}
</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 PointsToggle from '@/common/components/annotations/PointsToggle';
import useVideo from '@/common/components/video/editor/useVideo';
import useReportError from '@/common/error/useReportError';
import {
activeTrackletObjectIdAtom,
isPlayingAtom,
isStreamingAtom,
} from '@/demo/atoms';
import {
AddFilled,
Select_02,
SubtractFilled,
TrashCan,
} from '@carbon/icons-react';
import {useAtom, useAtomValue} from 'jotai';
import {useState} from 'react';
import type {ButtonProps} from 'react-daisyui';
import {Button} from 'react-daisyui';
type Props = {
objectId: number;
active: boolean;
};
function CustomButton({className, ...props}: ButtonProps) {
return (
<Button
size="sm"
color="ghost"
className={`font-medium border-none hover:bg-black px-2 h-10 ${className}`}
{...props}>
{props.children}
</Button>
);
}
export default function ObjectActions({objectId, active}: Props) {
const [isRemovingObject, setIsRemovingObject] = useState<boolean>(false);
const [activeTrackId, setActiveTrackletId] = useAtom(
activeTrackletObjectIdAtom,
);
const isStreaming = useAtomValue(isStreamingAtom);
const isPlaying = useAtom(isPlayingAtom);
const video = useVideo();
const reportError = useReportError();
async function handleRemoveObject(
event: React.MouseEvent<HTMLButtonElement>,
) {
try {
event.stopPropagation();
setIsRemovingObject(true);
if (isStreaming) {
await video?.abortStreamMasks();
}
if (isPlaying) {
video?.pause();
}
await video?.deleteTracklet(objectId);
} catch (error) {
reportError(error);
} finally {
setIsRemovingObject(false);
if (activeTrackId === objectId) {
setActiveTrackletId(null);
}
}
}
return (
<div>
{active && (
<div className="text-sm mt-1 leading-snug text-gray-400 hidden md:block ml-2 md:mb-4">
Select <AddFilled size={14} className="inline" /> to add areas to the
object and <SubtractFilled size={14} className="inline" /> to remove
areas from the object in the video. Click on an existing point to
delete it.
</div>
)}
<div className="flex justify-between items-center md:mt-2 mt-0">
{active ? (
<PointsToggle />
) : (
<>
<CustomButton startIcon={<Select_02 size={24} />}>
Edit selection
</CustomButton>
<CustomButton
loading={isRemovingObject}
onClick={handleRemoveObject}
startIcon={!isRemovingObject && <TrashCan size={24} />}>
<span className="hidden md:inline">Clear</span>
</CustomButton>
</>
)}
</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 {BLUE_PINK_FILL_BR} from '@/theme/gradientStyle';
type Props = {
showPlus?: boolean;
onClick?: () => void;
};
export default function ObjectPlaceholder({showPlus = true, onClick}: Props) {
return (
<div
className={`relative ${BLUE_PINK_FILL_BR} h-12 w-12 md:h-20 md:w-20 shrink-0 rounded-lg`}
onClick={onClick}>
{showPlus && (
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none">
<path
d="M16 7H9V0H7V7H0V9H7V16H9V9H16V7Z"
fill="#667788"
fillOpacity={1}
/>
</svg>
</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.
*/
type Props = {
thumbnail: string | null;
color: string;
onClick?: () => void;
};
export default function ObjectThumbnail({thumbnail, color, onClick}: Props) {
return (
<div
className="relative h-12 w-12 md:w-20 md:h-20 shrink-0 p-2 rounded-lg bg-contain bg-no-repeat bg-center"
style={{
backgroundColor: color,
}}
onClick={onClick}>
<div
className="w-full h-full bg-contain bg-no-repeat bg-center"
style={{
backgroundImage: thumbnail == null ? 'none' : `url(${thumbnail})`,
}}></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 {BaseTracklet} from '@/common/tracker/Tracker';
export function getObjectLabel(tracklet: BaseTracklet) {
return `Object ${tracklet.id + 1}`;
}
/**
* 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 AddObjectButton from '@/common/components/annotations/AddObjectButton';
import FirstClickView from '@/common/components/annotations/FirstClickView';
import LimitNotice from '@/common/components/annotations/LimitNotice';
import ObjectsToolbarBottomActions from '@/common/components/annotations/ObjectsToolbarBottomActions';
import ObjectsToolbarHeader from '@/common/components/annotations/ObjectsToolbarHeader';
import {getObjectLabel} from '@/common/components/annotations/ObjectUtils';
import ToolbarObject from '@/common/components/annotations/ToolbarObject';
import {
activeTrackletObjectAtom,
activeTrackletObjectIdAtom,
isAddObjectEnabledAtom,
isFirstClickMadeAtom,
isTrackletObjectLimitReachedAtom,
trackletObjectsAtom,
} from '@/demo/atoms';
import {useAtomValue, useSetAtom} from 'jotai';
type Props = {
onTabChange: (newIndex: number) => void;
};
export default function ObjectsToolbar({onTabChange}: Props) {
const tracklets = useAtomValue(trackletObjectsAtom);
const activeTracklet = useAtomValue(activeTrackletObjectAtom);
const setActiveTrackletId = useSetAtom(activeTrackletObjectIdAtom);
const isFirstClickMade = useAtomValue(isFirstClickMadeAtom);
const isObjectLimitReached = useAtomValue(isTrackletObjectLimitReachedAtom);
const isAddObjectEnabled = useAtomValue(isAddObjectEnabledAtom);
if (!isFirstClickMade) {
return <FirstClickView />;
}
return (
<div className="flex flex-col h-full">
<ObjectsToolbarHeader />
<div className="grow w-full overflow-y-auto">
{tracklets.map(tracklet => {
return (
<ToolbarObject
key={tracklet.id}
label={getObjectLabel(tracklet)}
tracklet={tracklet}
isActive={activeTracklet?.id === tracklet.id}
onClick={() => {
setActiveTrackletId(tracklet.id);
}}
/>
);
})}
{isAddObjectEnabled && <AddObjectButton />}
{isObjectLimitReached && <LimitNotice />}
</div>
<ObjectsToolbarBottomActions 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 ClearAllPointsInVideoButton from '@/common/components/annotations/ClearAllPointsInVideoButton';
import CloseSessionButton from '@/common/components/annotations/CloseSessionButton';
import TrackAndPlayButton from '@/common/components/button/TrackAndPlayButton';
import ToolbarBottomActionsWrapper from '@/common/components/toolbar/ToolbarBottomActionsWrapper';
import {
EFFECT_TOOLBAR_INDEX,
OBJECT_TOOLBAR_INDEX,
} from '@/common/components/toolbar/ToolbarConfig';
import {streamingStateAtom} from '@/demo/atoms';
import {useAtomValue} from 'jotai';
type Props = {
onTabChange: (newIndex: number) => void;
};
export default function ObjectsToolbarBottomActions({onTabChange}: Props) {
const streamingState = useAtomValue(streamingStateAtom);
const isTrackingEnabled =
streamingState !== 'none' && streamingState !== 'full';
function handleSwitchToEffectsTab() {
onTabChange(EFFECT_TOOLBAR_INDEX);
}
return (
<ToolbarBottomActionsWrapper>
<ClearAllPointsInVideoButton
onRestart={() => onTabChange(OBJECT_TOOLBAR_INDEX)}
/>
{isTrackingEnabled && <TrackAndPlayButton />}
{streamingState === 'full' && (
<CloseSessionButton onSessionClose={handleSwitchToEffectsTab} />
)}
</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 ToolbarHeaderWrapper from '@/common/components/toolbar/ToolbarHeaderWrapper';
import {isStreamingAtom, streamingStateAtom} from '@/demo/atoms';
import {useAtomValue} from 'jotai';
export default function ObjectsToolbarHeader() {
const isStreaming = useAtomValue(isStreamingAtom);
const streamingState = useAtomValue(streamingStateAtom);
return (
<ToolbarHeaderWrapper
title={
streamingState === 'full'
? 'Review tracked objects'
: isStreaming
? 'Tracking objects'
: 'Select objects'
}
description={
streamingState === 'full'
? 'Review your selected objects across the video, and continue to edit if needed. Once everything looks good, press “Next” to continue.'
: isStreaming
? 'Watch the video closely for any places where your objects aren’t tracked correctly. You can also stop tracking to make additional edits.'
: 'Adjust the selection of your object, or add additional objects. Press “Track objects” to track your objects throughout the video.'
}
className="mb-8"
/>
);
}
/**
* 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 {labelTypeAtom} from '@/demo/atoms';
import {AddFilled, SubtractFilled} from '@carbon/icons-react';
import {useAtom} from 'jotai';
export default function PointsToggle() {
const [labelType, setLabelType] = useAtom(labelTypeAtom);
const isPositive = labelType === 'positive';
const buttonStyle = (selected: boolean) =>
`btn-md bg-graydark-800 !text-white md:px-2 lg:px-4 py-0.5 ${selected ? `border border-white hover:bg-graydark-800` : `border-graydark-700 hover:bg-graydark-700`}`;
return (
<div className="flex items-center w-full md:ml-2">
<div className="join group grow gap-[1px]">
<button
className={`w-1/2 btn join-item text-white ${buttonStyle(isPositive)}`}
onClick={() => setLabelType('positive')}>
<AddFilled size={24} className="text-blue-500" /> Add
</button>
<button
className={`w-1/2 btn join-item text-red-700 ${buttonStyle(!isPositive)}`}
onClick={() => setLabelType('negative')}>
<SubtractFilled size={24} className="text-red-400" />
Remove
</button>
</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 GradientBorder from '@/common/components/button/GradientBorder';
import type {ReactNode} from 'react';
type Props = {
disabled?: boolean;
endIcon?: ReactNode;
} & React.DOMAttributes<HTMLButtonElement>;
export default function PrimaryCTAButton({
children,
disabled,
endIcon,
...props
}: Props) {
return (
<GradientBorder disabled={disabled}>
<button
className={`btn ${disabled && 'btn-disabled'} !rounded-full !bg-black !text-white !border-none`}
{...props}>
{children}
{endIcon != null && endIcon}
</button>
</GradientBorder>
);
}
/**
* 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 ObjectActions from '@/common/components/annotations/ObjectActions';
import ObjectPlaceholder from '@/common/components/annotations/ObjectPlaceholder';
import ObjectThumbnail from '@/common/components/annotations/ObjectThumbnail';
import ToolbarObjectContainer from '@/common/components/annotations/ToolbarObjectContainer';
import useVideo from '@/common/components/video/editor/useVideo';
import {BaseTracklet} from '@/common/tracker/Tracker';
import emptyFunction from '@/common/utils/emptyFunction';
import {activeTrackletObjectIdAtom} from '@/demo/atoms';
import {useSetAtom} from 'jotai';
type Props = {
label: string;
tracklet: BaseTracklet;
isActive: boolean;
isMobile?: boolean;
onClick?: () => void;
onThumbnailClick?: () => void;
};
export default function ToolbarObject({
label,
tracklet,
isActive,
isMobile = false,
onClick,
onThumbnailClick = emptyFunction,
}: Props) {
const video = useVideo();
const setActiveTrackletId = useSetAtom(activeTrackletObjectIdAtom);
async function handleCancelNewObject() {
try {
await video?.deleteTracklet(tracklet.id);
} catch (error) {
reportError(error);
} finally {
setActiveTrackletId(null);
}
}
if (!tracklet.isInitialized) {
return (
<ToolbarObjectContainer
alignItems="center"
isActive={isActive}
title="New object"
subtitle="No object is currently selected. Click an object in the video."
thumbnail={<ObjectPlaceholder showPlus={false} />}
isMobile={isMobile}
onClick={onClick}
onCancel={handleCancelNewObject}
/>
);
}
return (
<ToolbarObjectContainer
isActive={isActive}
onClick={onClick}
title={label}
subtitle=""
thumbnail={
<ObjectThumbnail
thumbnail={tracklet.thumbnail}
color={tracklet.color}
onClick={onThumbnailClick}
/>
}
isMobile={isMobile}>
<ObjectActions objectId={tracklet.id} active={isActive} />
</ToolbarObjectContainer>
);
}
/**
* 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 {spacing} from '@/theme/tokens.stylex';
import {Close} from '@carbon/icons-react';
import stylex from '@stylexjs/stylex';
import {PropsWithChildren, ReactNode} from 'react';
const sharedStyles = stylex.create({
container: {
display: 'flex',
overflow: 'hidden',
cursor: 'pointer',
flexShrink: 0,
borderTop: 'none',
backgroundColor: {
'@media screen and (max-width: 768px)': '#000',
},
paddingHorizontal: {
default: spacing[8],
'@media screen and (max-width: 768px)': spacing[5],
},
paddingBottom: {
default: spacing[8],
'@media screen and (max-width: 768px)': 10,
},
},
activeContainer: {
background: '#000',
borderRadius: 16,
marginHorizontal: 16,
padding: {
default: spacing[4],
'@media screen and (max-width: 768px)': spacing[5],
},
marginBottom: {
default: spacing[8],
'@media screen and (max-width: 768px)': 0,
},
},
itemsCenter: {
alignItems: 'center',
},
rightColumn: {
marginStart: {
default: spacing[4],
'@media screen and (max-width: 768px)': 0,
},
flexGrow: 1,
alignItems: 'center',
},
});
type ToolbarObjectContainerProps = PropsWithChildren<{
alignItems?: 'top' | 'center';
isActive: boolean;
title: string;
subtitle: string;
thumbnail: ReactNode;
isMobile: boolean;
onCancel?: () => void;
onClick?: () => void;
}>;
export default function ToolbarObjectContainer({
alignItems = 'top',
children,
isActive,
title,
subtitle,
thumbnail,
isMobile,
onClick,
onCancel,
}: ToolbarObjectContainerProps) {
if (isMobile) {
return (
<div
onClick={onClick}
{...stylex.props(sharedStyles.container, sharedStyles.itemsCenter)}>
<div {...stylex.props(sharedStyles.rightColumn)}>{children}</div>
</div>
);
}
return (
<div
onClick={onClick}
{...stylex.props(
sharedStyles.container,
isActive && sharedStyles.activeContainer,
alignItems === 'center' && sharedStyles.itemsCenter,
)}>
{thumbnail}
<div {...stylex.props(sharedStyles.rightColumn)}>
<div className="text-md font-semibold ml-2">{title}</div>
{subtitle.length > 0 && (
<div className="text-sm text-gray-400 leading-5 mt-2 ml-2">
{subtitle}
</div>
)}
{children}
</div>
{onCancel != null && (
<div className="items-start self-stretch" onClick={onCancel}>
<Close size={32} />
</div>
)}
</div>
);
}
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