diff --git a/app/packages/core/src/plugins/SchemaIO/components/FrameLoaderView.tsx b/app/packages/core/src/plugins/SchemaIO/components/FrameLoaderView.tsx index 8c0e94fb1e..fb16b13d16 100644 --- a/app/packages/core/src/plugins/SchemaIO/components/FrameLoaderView.tsx +++ b/app/packages/core/src/plugins/SchemaIO/components/FrameLoaderView.tsx @@ -7,6 +7,8 @@ import { usePanelId, useSetPanelStateById } from "@fiftyone/spaces"; import { useTimeline } from "@fiftyone/playback/src/lib/use-timeline"; import _ from "lodash"; +const FRAME_LOADED_EVENT = "frames-loaded"; + export default function FrameLoaderView(props: ViewPropsType) { const { schema, path, data } = props; const { view = {} } = schema; @@ -16,15 +18,16 @@ export default function FrameLoaderView(props: ViewPropsType) { const setPanelState = useSetPanelStateById(true); const localIdRef = React.useRef(); const bufm = useRef(new BufferManager()); + const frameDataRef = useRef(null); useEffect(() => { localIdRef.current = Math.random().toString(36).substring(7); - if (data?.frames) - window.dispatchEvent( - new CustomEvent(`frames-loaded`, { - detail: { localId: localIdRef.current }, - }) - ); + if (data?.frames) frameDataRef.current = data.frames; + window.dispatchEvent( + new CustomEvent(FRAME_LOADED_EVENT, { + detail: { localId: localIdRef.current }, + }) + ); }, [data?.signature]); const loadRange = React.useCallback( @@ -41,15 +44,22 @@ export default function FrameLoaderView(props: ViewPropsType) { } return new Promise((resolve) => { - window.addEventListener(`frames-loaded`, (e) => { - if ( - e instanceof CustomEvent && - e.detail.localId === localIdRef.current - ) { - bufm.current.addNewRange(range); - resolve(); - } - }); + if (frameDataRef.current) { + bufm.current.addNewRange(range); + resolve(); + } else { + const onFramesLoaded = (e) => { + if ( + e instanceof CustomEvent && + e.detail.localId === localIdRef.current + ) { + window.removeEventListener(FRAME_LOADED_EVENT, onFramesLoaded); + bufm.current.addNewRange(range); + resolve(); + } + }; + window.addEventListener(FRAME_LOADED_EVENT, onFramesLoaded); + } }); } }, diff --git a/app/packages/core/src/plugins/SchemaIO/components/PlotlyView.tsx b/app/packages/core/src/plugins/SchemaIO/components/PlotlyView.tsx index d7a18b33c8..186a3a1cf7 100644 --- a/app/packages/core/src/plugins/SchemaIO/components/PlotlyView.tsx +++ b/app/packages/core/src/plugins/SchemaIO/components/PlotlyView.tsx @@ -212,7 +212,6 @@ export default function PlotlyView(props: ViewPropsType) { return ( diff --git a/app/packages/core/src/plugins/SchemaIO/components/TimelineView.tsx b/app/packages/core/src/plugins/SchemaIO/components/TimelineView.tsx new file mode 100644 index 0000000000..a5c2ad198c --- /dev/null +++ b/app/packages/core/src/plugins/SchemaIO/components/TimelineView.tsx @@ -0,0 +1,43 @@ +import React, { useMemo } from "react"; +import { Timeline, useCreateTimeline, useTimeline } from "@fiftyone/playback"; +import { ViewPropsType } from "../utils/types"; + +const DEFAULT_CONFIG = { loop: false }; + +export default function TimelineView(props: ViewPropsType) { + const { schema } = props; + const { view = {} } = schema; + const { timeline_name, loop, total_frames } = view; + + const providedConfig = { + loop, + totalFrames: total_frames, + }; + + const finalConfig = useMemo( + () => ({ ...DEFAULT_CONFIG, ...providedConfig }), + [providedConfig] + ); + if (!timeline_name) { + throw new Error("Timeline name is required"); + } + if (!finalConfig.totalFrames) { + throw new Error("Total frames is required"); + } + + return ; +} + +export const TimelineCreator = ({ timelineName, totalFrames, loop }) => { + const config = useMemo(() => ({ totalFrames, loop }), [totalFrames, loop]); + const { isTimelineInitialized } = useCreateTimeline({ + name: timelineName, + config, + }); + + if (!isTimelineInitialized) { + return null; + } + + return ; +}; diff --git a/app/packages/core/src/plugins/SchemaIO/components/index.ts b/app/packages/core/src/plugins/SchemaIO/components/index.ts index 6e2e35e567..6a8bba165c 100644 --- a/app/packages/core/src/plugins/SchemaIO/components/index.ts +++ b/app/packages/core/src/plugins/SchemaIO/components/index.ts @@ -33,8 +33,8 @@ export { default as ListView } from "./ListView"; export { default as LoadingView } from "./LoadingView"; export { default as MapView } from "./MapView"; export { default as MarkdownView } from "./MarkdownView"; -export { default as ModalView } from "./ModalView"; export { default as MediaPlayerView } from "./MediaPlayerView"; +export { default as ModalView } from "./ModalView"; export { default as ObjectView } from "./ObjectView"; export { default as OneOfView } from "./OneOfView"; export { default as PillBadgeView } from "./PillBadgeView"; @@ -48,8 +48,9 @@ export { default as SwitchView } from "./SwitchView"; export { default as TableView } from "./TableView"; export { default as TabsView } from "./TabsView"; export { default as TagsView } from "./TagsView"; -export { default as TextView } from "./TextView"; export { default as TextFieldView } from "./TextFieldView"; +export { default as TextView } from "./TextView"; +export { default as TimelineView } from "./TimelineView"; export { default as ToastView } from "./ToastView"; export { default as TupleView } from "./TupleView"; export { default as UnsupportedView } from "./UnsupportedView"; diff --git a/fiftyone/operators/types.py b/fiftyone/operators/types.py index 407e9956ff..66e49b83ce 100644 --- a/fiftyone/operators/types.py +++ b/fiftyone/operators/types.py @@ -2574,6 +2574,19 @@ def __init__(self, **kwargs): super().__init__(**kwargs) +class TimelineView(View): + """Represents a timeline for playing animations. + + Args: + timeline_name (None): the name of the timeline + total_frames (None): the total number of frames in the timeline + loop (False): whether to loop the timeline + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + class Container(BaseType): """Represents a base container for a container types."""