Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
ModelZoo
samurai_pytorch
Commits
75d93027
Commit
75d93027
authored
Mar 12, 2025
by
bailuo
Browse files
init
parents
Pipeline
#2479
failed with stages
in 0 seconds
Changes
318
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
1134 additions
and
0 deletions
+1134
-0
sam2/demo/frontend/src/common/components/Tooltip.tsx
sam2/demo/frontend/src/common/components/Tooltip.tsx
+41
-0
sam2/demo/frontend/src/common/components/annotations/AddObjectButton.tsx
...end/src/common/components/annotations/AddObjectButton.tsx
+49
-0
sam2/demo/frontend/src/common/components/annotations/ClearAllPointsInVideoButton.tsx
...on/components/annotations/ClearAllPointsInVideoButton.tsx
+81
-0
sam2/demo/frontend/src/common/components/annotations/CloseSessionButton.tsx
.../src/common/components/annotations/CloseSessionButton.tsx
+38
-0
sam2/demo/frontend/src/common/components/annotations/FirstClickView.tsx
...tend/src/common/components/annotations/FirstClickView.tsx
+49
-0
sam2/demo/frontend/src/common/components/annotations/LimitNotice.tsx
...rontend/src/common/components/annotations/LimitNotice.tsx
+30
-0
sam2/demo/frontend/src/common/components/annotations/MobileObjectsList.tsx
...d/src/common/components/annotations/MobileObjectsList.tsx
+78
-0
sam2/demo/frontend/src/common/components/annotations/MobileObjectsToolbar.tsx
...rc/common/components/annotations/MobileObjectsToolbar.tsx
+51
-0
sam2/demo/frontend/src/common/components/annotations/MobileObjectsToolbarHeader.tsx
...mon/components/annotations/MobileObjectsToolbarHeader.tsx
+36
-0
sam2/demo/frontend/src/common/components/annotations/ObjectActions.tsx
...ntend/src/common/components/annotations/ObjectActions.tsx
+116
-0
sam2/demo/frontend/src/common/components/annotations/ObjectPlaceholder.tsx
...d/src/common/components/annotations/ObjectPlaceholder.tsx
+46
-0
sam2/demo/frontend/src/common/components/annotations/ObjectThumbnail.tsx
...end/src/common/components/annotations/ObjectThumbnail.tsx
+37
-0
sam2/demo/frontend/src/common/components/annotations/ObjectUtils.ts
...frontend/src/common/components/annotations/ObjectUtils.ts
+20
-0
sam2/demo/frontend/src/common/components/annotations/ObjectsToolbar.tsx
...tend/src/common/components/annotations/ObjectsToolbar.tsx
+72
-0
sam2/demo/frontend/src/common/components/annotations/ObjectsToolbarBottomActions.tsx
...on/components/annotations/ObjectsToolbarBottomActions.tsx
+52
-0
sam2/demo/frontend/src/common/components/annotations/ObjectsToolbarHeader.tsx
...rc/common/components/annotations/ObjectsToolbarHeader.tsx
+43
-0
sam2/demo/frontend/src/common/components/annotations/PointsToggle.tsx
...ontend/src/common/components/annotations/PointsToggle.tsx
+44
-0
sam2/demo/frontend/src/common/components/annotations/PrimaryCTAButton.tsx
...nd/src/common/components/annotations/PrimaryCTAButton.tsx
+40
-0
sam2/demo/frontend/src/common/components/annotations/ToolbarObject.tsx
...ntend/src/common/components/annotations/ToolbarObject.tsx
+88
-0
sam2/demo/frontend/src/common/components/annotations/ToolbarObjectContainer.tsx
.../common/components/annotations/ToolbarObjectContainer.tsx
+123
-0
No files found.
Too many changes to show.
To preserve performance only
318 of 318+
files are displayed.
Plain diff
Email patch
sam2/demo/frontend/src/common/components/Tooltip.tsx
0 → 100644
View file @
75d93027
/**
* 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
>
);
}
sam2/demo/frontend/src/common/components/annotations/AddObjectButton.tsx
0 → 100644
View file @
75d93027
/**
* 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
>
);
}
sam2/demo/frontend/src/common/components/annotations/ClearAllPointsInVideoButton.tsx
0 → 100644
View file @
75d93027
/**
* 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
>
);
}
sam2/demo/frontend/src/common/components/annotations/CloseSessionButton.tsx
0 → 100644
View file @
75d93027
/**
* 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
>
);
}
sam2/demo/frontend/src/common/components/annotations/FirstClickView.tsx
0 → 100644
View file @
75d93027
/**
* 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
'
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
>
);
}
sam2/demo/frontend/src/common/components/annotations/LimitNotice.tsx
0 → 100644
View file @
75d93027
/**
* 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
>
);
}
sam2/demo/frontend/src/common/components/annotations/MobileObjectsList.tsx
0 → 100644
View file @
75d93027
/**
* 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
>
);
}
sam2/demo/frontend/src/common/components/annotations/MobileObjectsToolbar.tsx
0 → 100644
View file @
75d93027
/**
* 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
>
);
}
sam2/demo/frontend/src/common/components/annotations/MobileObjectsToolbarHeader.tsx
0 → 100644
View file @
75d93027
/**
* 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
>
);
}
sam2/demo/frontend/src/common/components/annotations/ObjectActions.tsx
0 → 100644
View file @
75d93027
/**
* 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
>
);
}
sam2/demo/frontend/src/common/components/annotations/ObjectPlaceholder.tsx
0 → 100644
View file @
75d93027
/**
* 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
>
);
}
sam2/demo/frontend/src/common/components/annotations/ObjectThumbnail.tsx
0 → 100644
View file @
75d93027
/**
* 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
>
);
}
sam2/demo/frontend/src/common/components/annotations/ObjectUtils.ts
0 → 100644
View file @
75d93027
/**
* 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
}
`
;
}
sam2/demo/frontend/src/common/components/annotations/ObjectsToolbar.tsx
0 → 100644
View file @
75d93027
/**
* 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
>
);
}
sam2/demo/frontend/src/common/components/annotations/ObjectsToolbarBottomActions.tsx
0 → 100644
View file @
75d93027
/**
* 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
>
);
}
sam2/demo/frontend/src/common/components/annotations/ObjectsToolbarHeader.tsx
0 → 100644
View file @
75d93027
/**
* 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"
/>
);
}
sam2/demo/frontend/src/common/components/annotations/PointsToggle.tsx
0 → 100644
View file @
75d93027
/**
* 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
>
);
}
sam2/demo/frontend/src/common/components/annotations/PrimaryCTAButton.tsx
0 → 100644
View file @
75d93027
/**
* 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
>
);
}
sam2/demo/frontend/src/common/components/annotations/ToolbarObject.tsx
0 → 100644
View file @
75d93027
/**
* 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
>
);
}
sam2/demo/frontend/src/common/components/annotations/ToolbarObjectContainer.tsx
0 → 100644
View file @
75d93027
/**
* 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
>
);
}
Prev
1
…
11
12
13
14
15
16
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment