From c3e1b1a32b1c1c8d7da641b3fdc414997a92a61e Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Wed, 24 Jul 2024 19:13:30 +0530 Subject: [PATCH] Enhancements to Camera Feed Component (#8140) --- src/CAREUI/display/NetworkSignal.tsx | 2 +- src/CAREUI/interactive/KeyboardShortcut.tsx | 42 ++- src/Components/CameraFeed/AssetBedSelect.tsx | 83 +++-- src/Components/CameraFeed/CameraFeed.tsx | 176 ++++++---- .../CameraFeed/CameraFeedWithBedPresets.tsx | 29 +- src/Components/CameraFeed/FeedButton.tsx | 32 +- src/Components/CameraFeed/FeedControls.tsx | 325 ++++++++++-------- src/Components/CameraFeed/useOperateCamera.ts | 39 ++- src/Components/Common/components/ButtonV2.tsx | 4 +- .../Diagnosis/DiagnosesListAccordion.tsx | 1 + .../ConsultationFeedTab.tsx | 190 +++++----- .../Facility/ConsultationDetails/index.tsx | 2 +- src/Locale/en/Common.json | 2 + 13 files changed, 536 insertions(+), 391 deletions(-) diff --git a/src/CAREUI/display/NetworkSignal.tsx b/src/CAREUI/display/NetworkSignal.tsx index 91ce6b58b4c..d241a37674a 100644 --- a/src/CAREUI/display/NetworkSignal.tsx +++ b/src/CAREUI/display/NetworkSignal.tsx @@ -45,7 +45,7 @@ export default function NetworkSignal({ strength, children }: Props) { i === 2 && "h-[15px]", // Whether to infill with strength color or not - strength > i ? "bg-current" : "bg-zinc-600", + strength > i ? "bg-current" : "bg-zinc-500/30", )} /> )) diff --git a/src/CAREUI/interactive/KeyboardShortcut.tsx b/src/CAREUI/interactive/KeyboardShortcut.tsx index 06ce149fb51..1d2bebeb316 100644 --- a/src/CAREUI/interactive/KeyboardShortcut.tsx +++ b/src/CAREUI/interactive/KeyboardShortcut.tsx @@ -2,32 +2,50 @@ import useKeyboardShortcut from "use-keyboard-shortcut"; import { classNames, isAppleDevice } from "../../Utils/utils"; interface Props { - children: React.ReactNode; + children?: React.ReactNode; shortcut: string[]; + altShortcuts?: string[][]; onTrigger: () => void; - shortcutSeperator?: string; helpText?: string; tooltipClassName?: string; } export default function KeyboardShortcut(props: Props) { - useKeyboardShortcut(props.shortcut, props.onTrigger, { - overrideSystem: true, - }); + useKeyboardShortcut(props.shortcut, props.onTrigger); + + if (!props.children) { + return null; + } return (
{props.children} - {props.helpText} - - {getShortcutKeyDescription(props.shortcut).join(" + ")} - + {props.helpText && ( + {props.helpText} + )} + {(props.altShortcuts || [props.shortcut]).map((shortcut, idx, arr) => ( + <> + + {shortcut.map((key, idx, keys) => ( + <> + {SHORTCUT_KEY_MAP[key] || key} + {idx !== keys.length - 1 && ( + + + )} + + ))} + + {idx !== arr.length - 1 && ( + or + )} + + ))}
); @@ -43,7 +61,3 @@ const SHORTCUT_KEY_MAP = { ArrowLeft: "←", ArrowRight: "→", } as Record; - -export const getShortcutKeyDescription = (shortcut: string[]) => { - return shortcut.map((key) => SHORTCUT_KEY_MAP[key] || key); -}; diff --git a/src/Components/CameraFeed/AssetBedSelect.tsx b/src/Components/CameraFeed/AssetBedSelect.tsx index 3d7b7ab0951..dafb28d133f 100644 --- a/src/Components/CameraFeed/AssetBedSelect.tsx +++ b/src/Components/CameraFeed/AssetBedSelect.tsx @@ -3,8 +3,11 @@ import { AssetBedModel } from "../Assets/AssetTypes"; import { Listbox, Transition } from "@headlessui/react"; import CareIcon from "../../CAREUI/icons/CareIcon"; import { classNames } from "../../Utils/utils"; +import { dropdownOptionClassNames } from "../Form/MultiSelectMenuV2"; +import ButtonV2 from "../Common/components/ButtonV2"; interface Props { + disabled?: boolean; options: AssetBedModel[]; value?: AssetBedModel; label?: (value: AssetBedModel) => string; @@ -15,34 +18,44 @@ export default function CameraPresetSelect(props: Props) { const label = props.label ?? defaultLabel; return ( <> -
- {/* Desktop View */} + {/* Desktop View */} +
{props.options .slice(0, props.options.length > 5 ? 4 : 5) - .map((option) => ( - - ))} + .map((option) => { + const selected = props.value?.id === option.id; + + return ( + props.onChange?.(option)} + border + size="small" + > + {label(option)} + {selected && ( + + )} + + ); + })} {props.options.length > 5 && ( o.id === props.value?.id)} /> )}
+ + {/* Mobile View */}
- {/* Mobile View */}
@@ -62,15 +75,15 @@ export const CameraPresetDropdown = (
@@ -80,38 +93,32 @@ export const CameraPresetDropdown = ( ? label(selected) : props.placeholder} + {selected && ( + + )} - + {options?.map((obj) => ( - `relative cursor-default select-none px-2 py-1 ${ - active ? "bg-zinc-700 text-white" : "text-zinc-400" - }` + className={(args) => + classNames(dropdownOptionClassNames(args), "px-2 py-1.5") } value={obj} > - {({ selected }) => ( - <> - - {label(obj)} - - - )} + {label(obj)} ))} diff --git a/src/Components/CameraFeed/CameraFeed.tsx b/src/Components/CameraFeed/CameraFeed.tsx index fdb1ccbc9f9..7c5cf7a8a19 100644 --- a/src/Components/CameraFeed/CameraFeed.tsx +++ b/src/Components/CameraFeed/CameraFeed.tsx @@ -4,20 +4,19 @@ import useOperateCamera, { PTZPayload } from "./useOperateCamera"; import usePlayer from "./usePlayer"; import { getStreamUrl } from "./utils"; import ReactPlayer from "react-player"; -import { classNames, isAppleDevice, isIOS } from "../../Utils/utils"; +import { classNames, isIOS } from "../../Utils/utils"; import FeedAlert, { FeedAlertState } from "./FeedAlert"; import FeedNetworkSignal from "./FeedNetworkSignal"; import NoFeedAvailable from "./NoFeedAvailable"; import FeedControls from "./FeedControls"; import FeedWatermark from "./FeedWatermark"; -import CareIcon from "../../CAREUI/icons/CareIcon"; import useFullscreen from "../../Common/hooks/useFullscreen"; +import useBreakpoints from "../../Common/hooks/useBreakpoints"; interface Props { children?: React.ReactNode; asset: AssetData; preset?: PTZPayload; - silent?: boolean; className?: string; // Callbacks onCameraPresetsObtained?: (presets: Record) => void; @@ -27,16 +26,16 @@ interface Props { constrolsDisabled?: boolean; shortcutsDisabled?: boolean; onMove?: () => void; - onReset?: () => void; + operate: ReturnType["operate"]; } export default function CameraFeed(props: Props) { const playerRef = useRef(null); const playerWrapperRef = useRef(null); const streamUrl = getStreamUrl(props.asset); + const inlineControls = useBreakpoints({ default: false, sm: true }); const player = usePlayer(streamUrl, playerRef); - const operate = useOperateCamera(props.asset.id, props.silent); const [isFullscreen, setFullscreen] = useFullscreen(); const [state, setState] = useState(); @@ -46,7 +45,10 @@ export default function CameraFeed(props: Props) { useEffect(() => { async function move(preset: PTZPayload) { setState("moving"); - const { res } = await operate({ type: "absolute_move", data: preset }); + const { res } = await props.operate({ + type: "absolute_move", + data: preset, + }); setTimeout(() => setState((s) => (s === "moving" ? undefined : s)), 4000); if (res?.status === 500) { setState("host_unreachable"); @@ -62,19 +64,19 @@ export default function CameraFeed(props: Props) { useEffect(() => { if (!props.onCameraPresetsObtained) return; async function getPresets(cb: (presets: Record) => void) { - const { res, data } = await operate({ type: "get_presets" }); + const { res, data } = await props.operate({ type: "get_presets" }); if (res?.ok && data) { cb((data as { result: Record }).result); } } getPresets(props.onCameraPresetsObtained); - }, [operate, props.onCameraPresetsObtained]); + }, [props.operate, props.onCameraPresetsObtained]); const initializeStream = useCallback(() => { player.initializeStream({ onSuccess: async () => { props.onStreamSuccess?.(); - const { res } = await operate({ type: "get_status" }); + const { res } = await props.operate({ type: "get_status" }); if (res?.status === 500) { setState("host_unreachable"); } @@ -88,31 +90,102 @@ export default function CameraFeed(props: Props) { const resetStream = () => { setState("loading"); - props.onReset?.(); initializeStream(); }; + const controls = !props.constrolsDisabled && ( + { + if (!value) { + setFullscreen(false); + return; + } + + if (isIOS) { + const element = document.querySelector("video"); + if (!element) { + return; + } + setFullscreen(true, element, true); + return; + } + + if (!playerRef.current) { + return; + } + + setFullscreen( + true, + playerWrapperRef.current || (playerRef.current as HTMLElement), + true, + ); + }} + onReset={resetStream} + onMove={async (data) => { + props.onMove?.(); + setState("moving"); + const { res } = await props.operate({ type: "relative_move", data }); + setTimeout(() => { + setState((state) => (state === "moving" ? undefined : state)); + }, 4000); + if (res?.status === 500) { + setState("host_unreachable"); + } + }} + /> + ); + return (
-
- {props.children} +
{ + if (player.status !== "playing") { + return "bg-black text-zinc-400"; + } + + if (isFullscreen) { + return "bg-zinc-900 text-white"; + } + + return "bg-zinc-500/20 text-zinc-800"; + })(), + )} + > +
+ {props.children} +
- - + {props.asset.name} {!isIOS && ( -
+
- -
+
{/* Notifications */} {player.status === "playing" && } @@ -177,7 +249,7 @@ export default function CameraFeed(props: Props) { ) : (
+ {!inlineControls && ( +
+ {controls} +
+ )}
); diff --git a/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx b/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx index 8ce9c9ef67f..7268397b81a 100644 --- a/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx +++ b/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx @@ -5,6 +5,8 @@ import useQuery from "../../Utils/request/useQuery"; import routes from "../../Redux/api"; import useSlug from "../../Common/hooks/useSlug"; import { CameraPresetDropdown } from "./AssetBedSelect"; +import useOperateCamera from "./useOperateCamera"; +import { classNames } from "../../Utils/utils"; interface Props { asset: AssetData; @@ -18,24 +20,29 @@ export default function LocationFeedTile(props: Props) { query: { limit: 100, facility, asset: props.asset?.id }, }); + const { operate, key } = useOperateCamera(props.asset.id, true); + return ( -
- {loading ? ( - loading presets... - ) : ( - +
+
); diff --git a/src/Components/CameraFeed/FeedButton.tsx b/src/Components/CameraFeed/FeedButton.tsx index f0e568d4ad4..e2ae2a8fe9e 100644 --- a/src/Components/CameraFeed/FeedButton.tsx +++ b/src/Components/CameraFeed/FeedButton.tsx @@ -4,7 +4,7 @@ import { classNames } from "../../Utils/utils"; interface Props { className?: string; children?: React.ReactNode; - readonly shortcut?: string[]; + shortcuts?: string[][]; onTrigger: () => void; helpText?: string; shortcutsDisabled?: boolean; @@ -15,7 +15,8 @@ export default function FeedButton(props: Props) { const child = (