From 35c13f7a9d8eb6f463dd889a82dd32682c20a3b5 Mon Sep 17 00:00:00 2001 From: Benjamin Kane Date: Fri, 4 Oct 2024 17:54:45 -0400 Subject: [PATCH] Video playback (#4878) * organizing * rm Tmp * cleanup * add example back * cleaning * useKeyEvents hook * after load handling * fix import * enable timeline * default to true * refreshers --- .../src/components/Modal/ImaVidLooker.tsx | 25 +-- .../core/src/components/Modal/ModalLooker.tsx | 203 ++++-------------- .../core/src/components/Modal/VideoLooker.tsx | 80 +++++++ .../core/src/components/Modal/hooks.ts | 21 ++ .../src/components/Modal/use-key-events.ts | 40 ++++ .../core/src/components/Modal/use-looker.ts | 109 ++++++++++ .../core/src/components/Modal/utils.ts | 7 + app/packages/looker/package.json | 3 + app/packages/looker/src/elements/base.ts | 3 +- .../looker/src/elements/common/looker.ts | 14 +- app/packages/looker/src/index.ts | 2 +- .../looker/src/lookers/imavid/index.ts | 2 + app/packages/looker/src/lookers/video.ts | 13 ++ app/packages/looker/src/state.ts | 4 +- .../state/src/hooks/useCreateLooker.ts | 7 +- app/packages/state/src/recoil/modal.ts | 12 +- app/yarn.lock | 2 + 17 files changed, 347 insertions(+), 200 deletions(-) create mode 100644 app/packages/core/src/components/Modal/VideoLooker.tsx create mode 100644 app/packages/core/src/components/Modal/use-key-events.ts create mode 100644 app/packages/core/src/components/Modal/use-looker.ts create mode 100644 app/packages/core/src/components/Modal/utils.ts diff --git a/app/packages/core/src/components/Modal/ImaVidLooker.tsx b/app/packages/core/src/components/Modal/ImaVidLooker.tsx index 6707202a8a..952d6a1179 100644 --- a/app/packages/core/src/components/Modal/ImaVidLooker.tsx +++ b/app/packages/core/src/components/Modal/ImaVidLooker.tsx @@ -17,13 +17,14 @@ import React, { import { useErrorHandler } from "react-error-boundary"; import { useRecoilValue, useSetRecoilState } from "recoil"; import { v4 as uuid } from "uuid"; -import { useInitializeImaVidSubscriptions, useModalContext } from "./hooks"; +import { useClearSelectedLabels, useShowOverlays } from "./ModalLooker"; import { - shortcutToHelpItems, - useClearSelectedLabels, + useInitializeImaVidSubscriptions, useLookerOptionsUpdate, - useShowOverlays, -} from "./ModalLooker"; + useModalContext, +} from "./hooks"; +import useKeyEvents from "./use-key-events"; +import { shortcutToHelpItems } from "./utils"; interface ImaVidLookerReactProps { sample: fos.ModalSample; @@ -132,19 +133,7 @@ export const ImaVidLookerReact = React.memo( useEventHandler(looker, "clear", useClearSelectedLabels()); - const hoveredSample = useRecoilValue(fos.hoveredSample); - - useEffect(() => { - const hoveredSampleId = hoveredSample?._id; - looker.updater((state) => ({ - ...state, - // todo: always setting it to true might not be wise - shouldHandleKeyEvents: true, - options: { - ...state.options, - }, - })); - }, [hoveredSample, sample, looker]); + useKeyEvents(initialRef, sample._id, looker); const ref = useRef(null); useEffect(() => { diff --git a/app/packages/core/src/components/Modal/ModalLooker.tsx b/app/packages/core/src/components/Modal/ModalLooker.tsx index c18eb5e048..08b52c7ca0 100644 --- a/app/packages/core/src/components/Modal/ModalLooker.tsx +++ b/app/packages/core/src/components/Modal/ModalLooker.tsx @@ -1,35 +1,12 @@ import { useTheme } from "@fiftyone/components"; -import { AbstractLooker } from "@fiftyone/looker"; -import { BaseState } from "@fiftyone/looker/src/state"; +import type { ImageLooker } from "@fiftyone/looker"; import * as fos from "@fiftyone/state"; -import { useEventHandler, useOnSelectLabel } from "@fiftyone/state"; -import React, { useEffect, useMemo, useRef, useState } from "react"; -import { useErrorHandler } from "react-error-boundary"; +import React, { useEffect, useMemo } from "react"; import { useRecoilCallback, useRecoilValue, useSetRecoilState } from "recoil"; -import { v4 as uuid } from "uuid"; -import { useModalContext } from "./hooks"; import { ImaVidLookerReact } from "./ImaVidLooker"; - -export const useLookerOptionsUpdate = () => { - return useRecoilCallback( - ({ snapshot, set }) => - async (update: object, updater?: (updated: {}) => void) => { - const currentOptions = await snapshot.getPromise( - fos.savedLookerOptions - ); - - const panels = await snapshot.getPromise(fos.lookerPanels); - const updated = { - ...currentOptions, - ...update, - showJSON: panels.json.isOpen, - showHelp: panels.help.isOpen, - }; - set(fos.savedLookerOptions, updated); - if (updater) updater(updated); - } - ); -}; +import { VideoLookerReact } from "./VideoLooker"; +import { useModalContext } from "./hooks"; +import useLooker from "./use-looker"; export const useShowOverlays = () => { return useRecoilCallback(({ set }) => async (event: CustomEvent) => { @@ -47,137 +24,40 @@ export const useClearSelectedLabels = () => { }; interface LookerProps { - sample?: fos.ModalSample; - onClick?: React.MouseEventHandler; + sample: fos.ModalSample; } -const ModalLookerNoTimeline = React.memo( - ({ sample: sampleDataWithExtraParams }: LookerProps) => { - const [id] = useState(() => uuid()); - const colorScheme = useRecoilValue(fos.colorScheme); - - const { sample } = sampleDataWithExtraParams; - - const theme = useTheme(); - const initialRef = useRef(true); - const lookerOptions = fos.useLookerOptions(true); - const [reset, setReset] = useState(false); - const selectedMediaField = useRecoilValue(fos.selectedMediaField(true)); - const setModalLooker = useSetRecoilState(fos.modalLooker); - - const createLooker = fos.useCreateLooker(true, false, { - ...lookerOptions, - }); - - const { setActiveLookerRef } = useModalContext(); - - const looker = React.useMemo( - () => createLooker.current(sampleDataWithExtraParams), - [reset, createLooker, selectedMediaField] - ) as AbstractLooker; - - useEffect(() => { - setModalLooker(looker); - }, [looker]); - - useEffect(() => { - if (looker) { - setActiveLookerRef(looker as fos.Lookers); - } - }, [looker]); - - useEffect(() => { - !initialRef.current && looker.updateOptions(lookerOptions); - }, [lookerOptions]); - - useEffect(() => { - !initialRef.current && looker.updateSample(sample); - }, [sample, colorScheme]); - - useEffect(() => { - return () => looker?.destroy(); - }, [looker]); - - const handleError = useErrorHandler(); +const ModalLookerNoTimeline = React.memo((props: LookerProps) => { + const { id, looker, ref } = useLooker(props); + const theme = useTheme(); + const setModalLooker = useSetRecoilState(fos.modalLooker); - const updateLookerOptions = useLookerOptionsUpdate(); - useEventHandler(looker, "options", (e) => updateLookerOptions(e.detail)); - useEventHandler(looker, "showOverlays", useShowOverlays()); - useEventHandler(looker, "reset", () => { - setReset((c) => !c); - }); + const { setActiveLookerRef } = useModalContext(); - const jsonPanel = fos.useJSONPanel(); - const helpPanel = fos.useHelpPanel(); + useEffect(() => { + setModalLooker(looker); + }, [looker, setModalLooker]); - useEventHandler(looker, "select", useOnSelectLabel()); - useEventHandler(looker, "error", (event) => handleError(event.detail)); - useEventHandler( - looker, - "panels", - async ({ detail: { showJSON, showHelp, SHORTCUTS } }) => { - if (showJSON) { - jsonPanel[showJSON](sample); - } - if (showHelp) { - if (showHelp == "close") { - helpPanel.close(); - } else { - helpPanel[showHelp](shortcutToHelpItems(SHORTCUTS)); - } - } - - updateLookerOptions({}, (updatedOptions) => - looker.updateOptions(updatedOptions) - ); - } - ); - - useEffect(() => { - initialRef.current = false; - }, []); - - useEffect(() => { - looker.attach(id); - }, [looker, id]); - - useEventHandler(looker, "clear", useClearSelectedLabels()); - - const hoveredSample = useRecoilValue(fos.hoveredSample); - - useEffect(() => { - const hoveredSampleId = hoveredSample?._id; - looker.updater((state) => ({ - ...state, - shouldHandleKeyEvents: hoveredSampleId === sample._id, - options: { - ...state.options, - }, - })); - }, [hoveredSample, sample, looker]); - - const ref = useRef(null); - useEffect(() => { - ref.current?.dispatchEvent( - new CustomEvent(`looker-attached`, { bubbles: true }) - ); - }, [ref]); - - return ( -
- ); - } -); + useEffect(() => { + if (looker) { + setActiveLookerRef(looker as fos.Lookers); + } + }, [looker, setActiveLookerRef]); + + return ( +
+ ); +}); export const ModalLooker = React.memo( ({ sample: propsSampleData }: LookerProps) => { @@ -197,21 +77,16 @@ export const ModalLooker = React.memo( const shouldRenderImavid = useRecoilValue( fos.shouldRenderImaVidLooker(true) ); + const video = useRecoilValue(fos.isVideoDataset); if (shouldRenderImavid) { return ; } + if (video) { + return ; + } + return ; } ); - -export function shortcutToHelpItems(SHORTCUTS) { - return Object.values( - Object.values(SHORTCUTS).reduce((acc, v) => { - acc[v.shortcut] = v; - - return acc; - }, {}) - ); -} diff --git a/app/packages/core/src/components/Modal/VideoLooker.tsx b/app/packages/core/src/components/Modal/VideoLooker.tsx new file mode 100644 index 0000000000..9c8f9b0cd2 --- /dev/null +++ b/app/packages/core/src/components/Modal/VideoLooker.tsx @@ -0,0 +1,80 @@ +import { useTheme } from "@fiftyone/components"; +import type { VideoLooker } from "@fiftyone/looker"; +import { getFrameNumber } from "@fiftyone/looker"; +import { + useCreateTimeline, + useDefaultTimelineNameImperative, + useTimeline, +} from "@fiftyone/playback"; +import * as fos from "@fiftyone/state"; +import React, { useEffect, useMemo, useState } from "react"; +import useLooker from "./use-looker"; + +interface VideoLookerReactProps { + sample: fos.ModalSample; +} + +export const VideoLookerReact = (props: VideoLookerReactProps) => { + const theme = useTheme(); + const { id, looker, sample } = useLooker(props); + const [totalFrames, setTotalFrames] = useState(); + const frameRate = useMemo(() => { + return sample.frameRate; + }, [sample]); + + useEffect(() => { + const load = () => { + const duration = looker.getVideo().duration; + setTotalFrames(getFrameNumber(duration, duration, frameRate)); + looker.removeEventListener("load", load); + }; + looker.addEventListener("load", load); + }, [frameRate, looker]); + + return ( + <> +
+ {totalFrames !== undefined && ( + + )} + + ); +}; + +const TimelineController = ({ + looker, + totalFrames, +}: { + looker: VideoLooker; + totalFrames: number; +}) => { + const { getName } = useDefaultTimelineNameImperative(); + const timelineName = React.useMemo(() => getName(), [getName]); + + useCreateTimeline({ + name: timelineName, + config: totalFrames + ? { + totalFrames, + loop: true, + } + : undefined, + optOutOfAnimation: true, + }); + + const { pause, play } = useTimeline(timelineName); + + fos.useEventHandler(looker, "pause", pause); + fos.useEventHandler(looker, "play", play); + + return null; +}; diff --git a/app/packages/core/src/components/Modal/hooks.ts b/app/packages/core/src/components/Modal/hooks.ts index 700955dd47..2cdb6d6310 100644 --- a/app/packages/core/src/components/Modal/hooks.ts +++ b/app/packages/core/src/components/Modal/hooks.ts @@ -19,6 +19,27 @@ export const useLookerHelpers = () => { }; }; +export const useLookerOptionsUpdate = () => { + return useRecoilCallback( + ({ snapshot, set }) => + async (update: object, updater?: (updated: {}) => void) => { + const currentOptions = await snapshot.getPromise( + fos.savedLookerOptions + ); + + const panels = await snapshot.getPromise(fos.lookerPanels); + const updated = { + ...currentOptions, + ...update, + showJSON: panels.json.isOpen, + showHelp: panels.help.isOpen, + }; + set(fos.savedLookerOptions, updated); + if (updater) updater(updated); + } + ); +}; + export const useInitializeImaVidSubscriptions = () => { const subscribeToImaVidStateChanges = useRecoilCallback( ({ set }) => diff --git a/app/packages/core/src/components/Modal/use-key-events.ts b/app/packages/core/src/components/Modal/use-key-events.ts new file mode 100644 index 0000000000..49a4ce313b --- /dev/null +++ b/app/packages/core/src/components/Modal/use-key-events.ts @@ -0,0 +1,40 @@ +import type { Lookers } from "@fiftyone/state"; +import { hoveredSample } from "@fiftyone/state"; +import type { MutableRefObject } from "react"; +import { useEffect, useRef } from "react"; +import { selector, useRecoilValue } from "recoil"; + +export const hoveredSampleId = selector({ + key: "hoveredSampleId", + get: ({ get }) => { + return get(hoveredSample)?._id; + }, +}); + +export default function ( + ref: MutableRefObject, + id: string, + looker: Lookers +) { + const hoveredId = useRecoilValue(hoveredSampleId); + const ready = useRef(false); + + useEffect(() => { + if (ref.current) { + // initial call should wait for load event + const update = () => { + looker.updateOptions({ + shouldHandleKeyEvents: id === hoveredId, + }); + ready.current = true; + + looker.removeEventListener("load", update); + }; + looker.addEventListener("load", update); + } else if (ready.current) { + looker.updateOptions({ + shouldHandleKeyEvents: id === hoveredId, + }); + } + }, [hoveredId, id, looker, ref]); +} diff --git a/app/packages/core/src/components/Modal/use-looker.ts b/app/packages/core/src/components/Modal/use-looker.ts new file mode 100644 index 0000000000..fa44065f95 --- /dev/null +++ b/app/packages/core/src/components/Modal/use-looker.ts @@ -0,0 +1,109 @@ +import * as fos from "@fiftyone/state"; +import React, { useEffect, useRef, useState } from "react"; +import { useErrorHandler } from "react-error-boundary"; +import { useRecoilValue } from "recoil"; +import { v4 as uuid } from "uuid"; +import { useClearSelectedLabels, useShowOverlays } from "./ModalLooker"; +import { useLookerOptionsUpdate } from "./hooks"; +import useKeyEvents from "./use-key-events"; +import { shortcutToHelpItems } from "./utils"; + +const CLOSE = "close"; + +function useLooker({ + sample, +}: { + sample: fos.ModalSample; +}) { + const [id] = useState(() => uuid()); + const initialRef = useRef(true); + const ref = useRef(null); + const [reset, setReset] = useState(false); + const lookerOptions = fos.useLookerOptions(true); + const createLooker = fos.useCreateLooker( + true, + false, + lookerOptions, + undefined, + true + ); + const selectedMediaField = useRecoilValue(fos.selectedMediaField(true)); + const colorScheme = useRecoilValue(fos.colorScheme); + const looker = React.useMemo(() => { + /** start refreshers */ + reset; + selectedMediaField; + /** end refreshers */ + + return createLooker.current(sample); + }, [createLooker, reset, sample, selectedMediaField]) as L; + const handleError = useErrorHandler(); + const updateLookerOptions = useLookerOptionsUpdate(); + + fos.useEventHandler(looker, "clear", useClearSelectedLabels()); + fos.useEventHandler(looker, "error", (event) => handleError(event.detail)); + fos.useEventHandler(looker, "options", (e) => updateLookerOptions(e.detail)); + fos.useEventHandler(looker, "reset", () => setReset((c) => !c)); + fos.useEventHandler(looker, "select", fos.useOnSelectLabel()); + fos.useEventHandler(looker, "showOverlays", useShowOverlays()); + + useEffect(() => { + !initialRef.current && looker.updateOptions(lookerOptions); + }, [looker, lookerOptions]); + + useEffect(() => { + /** start refreshers */ + colorScheme; + /** end refreshers */ + + !initialRef.current && looker.updateSample(sample); + }, [colorScheme, looker, sample]); + + useEffect(() => { + initialRef.current = false; + }, []); + + useEffect(() => { + ref.current?.dispatchEvent( + new CustomEvent("looker-attached", { bubbles: true }) + ); + }, []); + + useEffect(() => { + looker.attach(id); + }, [looker, id]); + + useEffect(() => { + return () => looker?.destroy(); + }, [looker]); + + const jsonPanel = fos.useJSONPanel(); + const helpPanel = fos.useHelpPanel(); + + fos.useEventHandler( + looker, + "panels", + async ({ detail: { showJSON, showHelp, SHORTCUTS } }) => { + if (showJSON) { + jsonPanel[showJSON](sample); + } + if (showHelp) { + if (showHelp === CLOSE) { + helpPanel.close(); + } else { + helpPanel[showHelp](shortcutToHelpItems(SHORTCUTS)); + } + } + + updateLookerOptions({}, (updatedOptions) => + looker.updateOptions(updatedOptions) + ); + } + ); + + useKeyEvents(initialRef, sample.sample._id, looker); + + return { id, looker, ref, sample, updateLookerOptions }; +} + +export default useLooker; diff --git a/app/packages/core/src/components/Modal/utils.ts b/app/packages/core/src/components/Modal/utils.ts new file mode 100644 index 0000000000..2461570f9a --- /dev/null +++ b/app/packages/core/src/components/Modal/utils.ts @@ -0,0 +1,7 @@ +export function shortcutToHelpItems(SHORTCUTS) { + const result = {}; + for (const k of SHORTCUTS) { + result[SHORTCUTS[k].shortcut] = SHORTCUTS[k]; + } + return Object.values(result); +} diff --git a/app/packages/looker/package.json b/app/packages/looker/package.json index b47bb84fd6..8ac5a0b945 100644 --- a/app/packages/looker/package.json +++ b/app/packages/looker/package.json @@ -39,5 +39,8 @@ "typescript": "^4.7.4", "typescript-plugin-css-modules": "^5.1.0", "vite": "^5.2.14" + }, + "peerDependencies": { + "jotai": "*" } } diff --git a/app/packages/looker/src/elements/base.ts b/app/packages/looker/src/elements/base.ts index e872a800c5..5ce470ef87 100644 --- a/app/packages/looker/src/elements/base.ts +++ b/app/packages/looker/src/elements/base.ts @@ -64,8 +64,7 @@ export abstract class BaseElement< for (const [eventType, handler] of Object.entries(this.getEvents(config))) { this.events[eventType] = (event) => handler({ event, update, dispatchEvent }); - this.element && - this.element.addEventListener(eventType, this.events[eventType]); + this.element?.addEventListener(eventType, this.events[eventType]); } } applyChildren(children: BaseElement[]) { diff --git a/app/packages/looker/src/elements/common/looker.ts b/app/packages/looker/src/elements/common/looker.ts index 8910d90cd0..b483b178f2 100644 --- a/app/packages/looker/src/elements/common/looker.ts +++ b/app/packages/looker/src/elements/common/looker.ts @@ -3,8 +3,10 @@ */ import { SELECTION_TEXT } from "../../constants"; -import { BaseState, Control, ControlEventKeyType } from "../../state"; -import { BaseElement, Events } from "../base"; +import type { BaseState, Control } from "../../state"; +import { ControlEventKeyType } from "../../state"; +import type { Events } from "../base"; +import { BaseElement } from "../base"; import { looker, lookerError, lookerHighlight } from "./looker.module.css"; @@ -24,7 +26,11 @@ export class LookerElement extends BaseElement< const e = event as KeyboardEvent; update((state) => { - const { SHORTCUTS, error, shouldHandleKeyEvents } = state; + const { + SHORTCUTS, + error, + options: { shouldHandleKeyEvents }, + } = state; if (!error && e.key in SHORTCUTS) { const matchedControl = SHORTCUTS[e.key] as Control; const enabled = @@ -43,7 +49,7 @@ export class LookerElement extends BaseElement< } const e = event as KeyboardEvent; - update(({ SHORTCUTS, error, shouldHandleKeyEvents }) => { + update(({ SHORTCUTS, error, options: { shouldHandleKeyEvents } }) => { if (!error && e.key in SHORTCUTS) { const matchedControl = SHORTCUTS[e.key] as Control; diff --git a/app/packages/looker/src/index.ts b/app/packages/looker/src/index.ts index 333140735d..889ec3ff73 100644 --- a/app/packages/looker/src/index.ts +++ b/app/packages/looker/src/index.ts @@ -3,7 +3,7 @@ */ export { createColorGenerator, getRGB } from "@fiftyone/utilities"; -export { freeVideos } from "./elements/util"; +export { freeVideos, getFrameNumber } from "./elements/util"; export * from "./lookers"; export type { PointInfo } from "./overlays"; export type { diff --git a/app/packages/looker/src/lookers/imavid/index.ts b/app/packages/looker/src/lookers/imavid/index.ts index ae3d60d1d2..cd302395e4 100644 --- a/app/packages/looker/src/lookers/imavid/index.ts +++ b/app/packages/looker/src/lookers/imavid/index.ts @@ -15,6 +15,8 @@ import { IMAVID_PLAYBACK_RATE_LOCAL_STORAGE_KEY, } from "./constants"; +export { BUFFERING_PAUSE_TIMEOUT } from "./constants"; + const DEFAULT_PAN = 0; const DEFAULT_SCALE = 1; const FIRST_FRAME = 1; diff --git a/app/packages/looker/src/lookers/video.ts b/app/packages/looker/src/lookers/video.ts index e07771fe7e..0307594409 100644 --- a/app/packages/looker/src/lookers/video.ts +++ b/app/packages/looker/src/lookers/video.ts @@ -20,7 +20,9 @@ import { } from "../state"; import { addToBuffers, createWorker, removeFromBuffers } from "../util"; +import { setFrameNumberAtom } from "@fiftyone/playback"; import { Schema } from "@fiftyone/utilities"; +import { getDefaultStore } from "jotai"; import { LRUCache } from "lru-cache"; import { CHUNK_SIZE, MAX_FRAME_CACHE_SIZE_BYTES } from "../constants"; import { getFrameNumber } from "../elements/util"; @@ -525,6 +527,13 @@ export class VideoLooker extends AbstractLooker { this.state.setZoom = false; } + if (this.state.config.enableTimeline) { + getDefaultStore().set(setFrameNumberAtom, { + name: `timeline-${this.state.config.sampleId}`, + newFrameNumber: this.state.frameNumber, + }); + } + return super.postProcess(); } @@ -546,6 +555,10 @@ export class VideoLooker extends AbstractLooker { this.setReader(); } + getVideo() { + return this.lookerElement.children[0].element as HTMLVideoElement; + } + private hasFrame(frameNumber: number) { return ( this.frames.has(frameNumber) && diff --git a/app/packages/looker/src/state.ts b/app/packages/looker/src/state.ts index 64dfe21278..84281f154a 100644 --- a/app/packages/looker/src/state.ts +++ b/app/packages/looker/src/state.ts @@ -175,6 +175,7 @@ interface BaseOptions { smoothMasks: boolean; zoomPad: number; selected: boolean; + shouldHandleKeyEvents?: boolean; inSelectionMode: boolean; timeZone: string; mimetype: string; @@ -218,6 +219,7 @@ export interface FrameConfig extends BaseConfig { export type ImageConfig = BaseConfig; export interface VideoConfig extends BaseConfig { + enableTimeline: boolean; frameRate: number; support?: [number, number]; } @@ -297,7 +299,6 @@ export interface BaseState { showOptions: boolean; config: BaseConfig; options: BaseOptions; - shouldHandleKeyEvents: boolean; scale: number; pan: Coordinates; panning: boolean; @@ -458,6 +459,7 @@ export const DEFAULT_BASE_OPTIONS: BaseOptions = { pointFilter: (path: string, point: Point) => true, attributeVisibility: {}, mediaFallback: false, + shouldHandleKeyEvents: true, }; export const DEFAULT_FRAME_OPTIONS: FrameOptions = { diff --git a/app/packages/state/src/hooks/useCreateLooker.ts b/app/packages/state/src/hooks/useCreateLooker.ts index 06e04eb652..1fc1b748e3 100644 --- a/app/packages/state/src/hooks/useCreateLooker.ts +++ b/app/packages/state/src/hooks/useCreateLooker.ts @@ -9,7 +9,7 @@ import { } from "@fiftyone/looker"; import { ImaVidFramesController } from "@fiftyone/looker/src/lookers/imavid/controller"; import { ImaVidFramesControllerStore } from "@fiftyone/looker/src/lookers/imavid/store"; -import { BaseState, ImaVidConfig } from "@fiftyone/looker/src/state"; +import type { BaseState, ImaVidConfig } from "@fiftyone/looker/src/state"; import { EMBEDDED_DOCUMENT_FIELD, LIST_FIELD, @@ -36,7 +36,8 @@ export default >( isModal: boolean, thumbnail: boolean, options: Omit[0], "selected">, - highlight?: (sample: Sample) => boolean + highlight?: (sample: Sample) => boolean, + enableTimeline?: boolean ) => { const environment = useRelayEnvironment(); const selected = useRecoilValue(selectedSamples); @@ -112,6 +113,7 @@ export default >( } let config: ConstructorParameters[1] = { + enableTimeline, fieldSchema: { ...fieldSchema, frames: { @@ -132,6 +134,7 @@ export default >( mediaField, thumbnail, view, + shouldHandleKeyEvents: isModal, }; let sampleMediaFilePath = urls[mediaField]; diff --git a/app/packages/state/src/recoil/modal.ts b/app/packages/state/src/recoil/modal.ts index 2a79313a37..52baab87c2 100644 --- a/app/packages/state/src/recoil/modal.ts +++ b/app/packages/state/src/recoil/modal.ts @@ -1,13 +1,9 @@ -import { - AbstractLooker, - BaseState, - PointInfo, - type Sample, -} from "@fiftyone/looker"; +import { PointInfo, type Sample } from "@fiftyone/looker"; import { mainSample, mainSampleQuery } from "@fiftyone/relay"; import { atom, selector } from "recoil"; import { graphQLSelector } from "recoil-relay"; import { VariablesOf } from "relay-runtime"; +import type { Lookers } from "../hooks"; import { ComputeCoordinatesReturnType } from "../hooks/useTooltip"; import { ModalSelector, sessionAtom } from "../session"; import { ResponseFrom } from "../utils"; @@ -27,7 +23,7 @@ import { datasetName } from "./selectors"; import { mapSampleResponse } from "./utils"; import { view } from "./view"; -export const modalLooker = atom | null>({ +export const modalLooker = atom({ key: "modalLooker", default: null, dangerouslyAllowMutability: true, @@ -73,7 +69,7 @@ export const currentSampleId = selector({ ? get(pinned3DSample).id : get(nullableModalSampleId); - if (id && id.endsWith("-modal")) { + if (id?.endsWith("-modal")) { return id.replace("-modal", ""); } return id; diff --git a/app/yarn.lock b/app/yarn.lock index dc9917db4b..86e96c90f8 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -1885,6 +1885,8 @@ __metadata: typescript-plugin-css-modules: ^5.1.0 uuid: ^8.3.2 vite: ^5.2.14 + peerDependencies: + jotai: "*" languageName: unknown linkType: soft