ui(components): CinePlayer updated for ui-next (#4932)
This commit is contained in:
parent
f71d370969
commit
1b8729dded
@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useEffect, useState, useRef } from 'react';
|
||||
import { useCine } from '@ohif/ui';
|
||||
import { useCine } from '@ohif/ui-next';
|
||||
import { Enums, eventTarget, cache } from '@cornerstonejs/core';
|
||||
import { useAppConfig } from '@state';
|
||||
|
||||
@ -244,4 +244,4 @@ function RenderCinePlayer({
|
||||
);
|
||||
}
|
||||
|
||||
export default WrappedCinePlayer;
|
||||
export default WrappedCinePlayer;
|
||||
@ -1,4 +1,4 @@
|
||||
import { CinePlayer } from '@ohif/ui';
|
||||
import { CinePlayer } from '@ohif/ui-next';
|
||||
import DicomUpload from '../components/DicomUpload/DicomUpload';
|
||||
|
||||
export default {
|
||||
|
||||
@ -14,17 +14,13 @@ import {
|
||||
ServiceProvidersManager,
|
||||
SystemContextProvider,
|
||||
} from '@ohif/core';
|
||||
import {
|
||||
ThemeWrapper,
|
||||
ViewportDialogProvider,
|
||||
CineProvider,
|
||||
UserAuthenticationProvider,
|
||||
} from '@ohif/ui';
|
||||
import { ThemeWrapper, ViewportDialogProvider, UserAuthenticationProvider } from '@ohif/ui';
|
||||
import {
|
||||
ThemeWrapper as ThemeWrapperNext,
|
||||
NotificationProvider,
|
||||
ViewportGridProvider,
|
||||
DialogProvider,
|
||||
CineProvider,
|
||||
TooltipProvider,
|
||||
Modal as ModalNext,
|
||||
ManagedDialog,
|
||||
|
||||
199
platform/ui-next/src/components/CinePlayer/CinePlayer.tsx
Normal file
199
platform/ui-next/src/components/CinePlayer/CinePlayer.tsx
Normal file
@ -0,0 +1,199 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import debounce from 'lodash.debounce';
|
||||
|
||||
import { Icons } from '@ohif/ui-next';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '../Popover/Popover';
|
||||
import { Button } from '../Button/Button';
|
||||
import { Numeric } from '../Numeric/Numeric';
|
||||
|
||||
export type CinePlayerProps = {
|
||||
className: string;
|
||||
isPlaying: boolean;
|
||||
minFrameRate?: number;
|
||||
maxFrameRate?: number;
|
||||
stepFrameRate?: number;
|
||||
frameRate?: number;
|
||||
onFrameRateChange: (value: number) => void;
|
||||
onPlayPauseChange: (value: boolean) => void;
|
||||
onClose: () => void;
|
||||
updateDynamicInfo?: (info: any) => void;
|
||||
dynamicInfo?: {
|
||||
dimensionGroupNumber: number;
|
||||
numDimensionGroups: number;
|
||||
label?: string;
|
||||
};
|
||||
};
|
||||
|
||||
const CinePlayer: React.FC<CinePlayerProps> = ({
|
||||
className,
|
||||
isPlaying = false,
|
||||
minFrameRate = 1,
|
||||
maxFrameRate = 90,
|
||||
stepFrameRate = 1,
|
||||
frameRate: defaultFrameRate = 24,
|
||||
onFrameRateChange = () => {},
|
||||
onPlayPauseChange = () => {},
|
||||
onClose = () => {},
|
||||
dynamicInfo = {},
|
||||
updateDynamicInfo,
|
||||
}) => {
|
||||
const isDynamic = !!dynamicInfo?.numDimensionGroups;
|
||||
const [frameRate, setFrameRate] = useState(defaultFrameRate);
|
||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||
const debouncedSetFrameRate = useCallback(debounce(onFrameRateChange, 100), [onFrameRateChange]);
|
||||
|
||||
const getPlayPauseIconName = () => (isPlaying ? 'icon-pause' : 'icon-play');
|
||||
|
||||
const handleSetFrameRate = (frameRate: number) => {
|
||||
if (frameRate < minFrameRate || frameRate > maxFrameRate) {
|
||||
return;
|
||||
}
|
||||
setFrameRate(frameRate);
|
||||
debouncedSetFrameRate(frameRate);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setFrameRate(defaultFrameRate);
|
||||
}, [defaultFrameRate]);
|
||||
|
||||
const handleDimensionGroupNumberChange = useCallback(
|
||||
(newGroupNumber: number) => {
|
||||
if (isDynamic && dynamicInfo) {
|
||||
updateDynamicInfo?.({
|
||||
...dynamicInfo,
|
||||
dimensionGroupNumber: newGroupNumber,
|
||||
});
|
||||
}
|
||||
},
|
||||
[isDynamic, dynamicInfo, updateDynamicInfo]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{isDynamic && dynamicInfo && (
|
||||
<Numeric.Container
|
||||
mode="singleRange"
|
||||
min={1}
|
||||
max={dynamicInfo.numDimensionGroups}
|
||||
step={1}
|
||||
value={dynamicInfo.dimensionGroupNumber}
|
||||
onChange={val => handleDimensionGroupNumberChange(val as number)}
|
||||
className="mb-3 w-full"
|
||||
>
|
||||
<Numeric.SingleRange showNumberInput={false} />
|
||||
</Numeric.Container>
|
||||
)}
|
||||
<div className={'bg-muted inline-flex select-none items-center gap-2 rounded-md px-2 py-2'}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onPlayPauseChange(!isPlaying)}
|
||||
data-cy={'cine-player-play-pause'}
|
||||
>
|
||||
<Icons.ByName name={getPlayPauseIconName()} />
|
||||
</Button>
|
||||
|
||||
{isDynamic && dynamicInfo && (
|
||||
<div className="min-w-16 max-w-44 text-foreground flex flex-col">
|
||||
<div className="text-xs">
|
||||
<span className="text-foreground w-2">{dynamicInfo.dimensionGroupNumber}</span>{' '}
|
||||
<span className="text-muted-foreground">{`/${dynamicInfo.numDimensionGroups}`}</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs">{dynamicInfo.label}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Popover
|
||||
open={popoverOpen}
|
||||
onOpenChange={setPopoverOpen}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-full border-none bg-transparent p-0 hover:bg-transparent"
|
||||
>
|
||||
<Numeric.Container
|
||||
mode="stepper"
|
||||
min={minFrameRate}
|
||||
max={maxFrameRate}
|
||||
step={stepFrameRate}
|
||||
value={frameRate}
|
||||
onChange={val => handleSetFrameRate(val as number)}
|
||||
className="border-0 bg-transparent"
|
||||
>
|
||||
<Numeric.NumberStepper
|
||||
direction="horizontal"
|
||||
inputWidth="w-7 max-w-7"
|
||||
>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<div className="text-foreground flex-shrink-0 text-center text-sm leading-[22px]">
|
||||
<span className="text-muted-foreground whitespace-nowrap text-xs">
|
||||
{' FPS'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Numeric.NumberStepper>
|
||||
</Numeric.Container>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side="top"
|
||||
align="center"
|
||||
className="cine-fps-range-popover z-50 w-auto p-2"
|
||||
sideOffset={8}
|
||||
>
|
||||
<Numeric.Container
|
||||
mode="singleRange"
|
||||
min={minFrameRate}
|
||||
max={maxFrameRate}
|
||||
step={stepFrameRate}
|
||||
value={frameRate}
|
||||
onChange={val => handleSetFrameRate(val as number)}
|
||||
className="h-6 px-2"
|
||||
>
|
||||
<Numeric.SingleRange
|
||||
showNumberInput={false}
|
||||
sliderClassName="w-40"
|
||||
/>
|
||||
</Numeric.Container>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onClose}
|
||||
data-cy={'cine-player-close'}
|
||||
>
|
||||
<Icons.Close />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CinePlayer.propTypes = {
|
||||
/** Minimum value for range slider */
|
||||
minFrameRate: PropTypes.number,
|
||||
/** Maximum value for range slider */
|
||||
maxFrameRate: PropTypes.number,
|
||||
/** Increment range slider can "step" in either direction */
|
||||
stepFrameRate: PropTypes.number,
|
||||
frameRate: PropTypes.number,
|
||||
/** 'true' if playing, 'false' if paused */
|
||||
isPlaying: PropTypes.bool.isRequired,
|
||||
onPlayPauseChange: PropTypes.func,
|
||||
onFrameRateChange: PropTypes.func,
|
||||
onClose: PropTypes.func,
|
||||
isDynamic: PropTypes.bool,
|
||||
dynamicInfo: PropTypes.shape({
|
||||
dimensionGroupNumber: PropTypes.number,
|
||||
numDimensionGroups: PropTypes.number,
|
||||
label: PropTypes.string,
|
||||
}),
|
||||
};
|
||||
|
||||
export default CinePlayer;
|
||||
5
platform/ui-next/src/components/CinePlayer/index.ts
Normal file
5
platform/ui-next/src/components/CinePlayer/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import CinePlayer from './CinePlayer';
|
||||
import type { CinePlayerProps } from './CinePlayer';
|
||||
|
||||
export { CinePlayer, CinePlayerProps };
|
||||
export default CinePlayer;
|
||||
@ -312,10 +312,11 @@ interface NumberStepperProps {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
direction?: 'horizontal' | 'vertical';
|
||||
inputWidth?: string;
|
||||
}
|
||||
|
||||
// Modified NumberStepper component to properly position left/right controls
|
||||
function NumberStepper({ className, children, direction }: NumberStepperProps) {
|
||||
function NumberStepper({ className, children, direction, inputWidth }: NumberStepperProps) {
|
||||
const ctx = useContext(NumericMetaContext);
|
||||
if (!ctx) {
|
||||
throw new Error('NumberStepper must be used inside <Numeric.Container>.');
|
||||
@ -379,7 +380,10 @@ function NumberStepper({ className, children, direction }: NumberStepperProps) {
|
||||
value={displayValue}
|
||||
onChange={handleInputChange}
|
||||
onBlur={handleBlur}
|
||||
className="h-6 flex-1 appearance-none border-none p-0 text-center shadow-none focus:border-none focus:outline-none"
|
||||
className={cn(
|
||||
"h-6 appearance-none border-none p-0 text-center shadow-none focus:border-none focus:outline-none",
|
||||
inputWidth ? inputWidth : "w-12 max-w-12"
|
||||
)}
|
||||
/>
|
||||
{children}
|
||||
<RightControl
|
||||
@ -405,7 +409,8 @@ function NumberStepper({ className, children, direction }: NumberStepperProps) {
|
||||
onChange={handleInputChange}
|
||||
onBlur={handleBlur}
|
||||
className={cn(
|
||||
'h-6 flex-1 appearance-none border-none p-0 text-center shadow-none focus:border-none focus:outline-none'
|
||||
'h-6 appearance-none border-none p-0 text-center shadow-none focus:border-none focus:outline-none',
|
||||
inputWidth ? inputWidth : "w-12 max-w-12"
|
||||
)}
|
||||
/>
|
||||
<div className="ml-1 flex flex-col">
|
||||
|
||||
@ -29,6 +29,7 @@ import { Combobox } from './Combobox';
|
||||
import { Popover, PopoverContent, PopoverTrigger, PopoverAnchor } from './Popover';
|
||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from './Resizable';
|
||||
import { Calendar } from './Calendar';
|
||||
import CinePlayer from './CinePlayer';
|
||||
import { DatePickerWithRange } from './DateRange';
|
||||
import { Separator } from './Separator';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from './Tabs';
|
||||
@ -254,4 +255,5 @@ export {
|
||||
InputFilter,
|
||||
WindowLevel,
|
||||
WindowLevelHistogram,
|
||||
};
|
||||
CinePlayer,
|
||||
};
|
||||
103
platform/ui-next/src/contextProviders/CineProvider.tsx
Normal file
103
platform/ui-next/src/contextProviders/CineProvider.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
import React, { createContext, useCallback, useContext, useEffect, useReducer } from 'react';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
isCineEnabled: false,
|
||||
cines: {
|
||||
/*
|
||||
* viewportId: { isPlaying: false, frameRate: 24 };
|
||||
*/
|
||||
},
|
||||
};
|
||||
|
||||
const DEFAULT_CINE = { isPlaying: false, frameRate: 24 };
|
||||
|
||||
export const CineContext = createContext(null);
|
||||
|
||||
export default function CineProvider({ children, service }) {
|
||||
const reducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case 'SET_CINE': {
|
||||
const { id, frameRate, isPlaying = undefined } = action.payload;
|
||||
const cines = state.cines;
|
||||
|
||||
const syncedCineIds = service.getSyncedViewports(id).map(({ viewportId }) => viewportId);
|
||||
const cineIdsToUpdate = [id, ...syncedCineIds].filter(curId => {
|
||||
const currentCine = cines[curId] ?? {};
|
||||
const shouldUpdateFrameRate =
|
||||
currentCine.frameRate !== (frameRate ?? currentCine.frameRate);
|
||||
const shouldUpdateIsPlaying =
|
||||
currentCine.isPlaying !== (isPlaying ?? currentCine.isPlaying);
|
||||
|
||||
return shouldUpdateFrameRate || shouldUpdateIsPlaying;
|
||||
});
|
||||
|
||||
cineIdsToUpdate.forEach(currId => {
|
||||
let cine = cines[currId];
|
||||
|
||||
if (!cine) {
|
||||
cine = { id, ...DEFAULT_CINE };
|
||||
cines[currId] = cine;
|
||||
}
|
||||
|
||||
cine.frameRate = frameRate ?? cine.frameRate;
|
||||
cine.isPlaying = isPlaying ?? cine.isPlaying;
|
||||
});
|
||||
|
||||
return { ...state, ...cines };
|
||||
}
|
||||
case 'SET_IS_CINE_ENABLED': {
|
||||
return { ...state, ...{ isCineEnabled: action.payload } };
|
||||
}
|
||||
default:
|
||||
return action.payload;
|
||||
}
|
||||
};
|
||||
|
||||
const [state, dispatch] = useReducer(reducer, DEFAULT_STATE);
|
||||
|
||||
const getState = useCallback(() => state, [state]);
|
||||
|
||||
const setIsCineEnabled = useCallback(
|
||||
isCineEnabled => dispatch({ type: 'SET_IS_CINE_ENABLED', payload: isCineEnabled }),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const setCine = useCallback(
|
||||
({ id, frameRate, isPlaying }) =>
|
||||
dispatch({
|
||||
type: 'SET_CINE',
|
||||
payload: {
|
||||
id,
|
||||
frameRate,
|
||||
isPlaying,
|
||||
},
|
||||
}),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets the implementation of a modal service that can be used by extensions.
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (service) {
|
||||
service.setServiceImplementation({ getState, setIsCineEnabled, setCine });
|
||||
}
|
||||
}, [getState, service, setCine, setIsCineEnabled]);
|
||||
|
||||
const api = {
|
||||
getState,
|
||||
setCine,
|
||||
setIsCineEnabled: isCineEnabled => service.setIsCineEnabled(isCineEnabled),
|
||||
playClip: (element, playClipOptions) => service.playClip(element, playClipOptions),
|
||||
stopClip: (element, stopClipOptions) => service.stopClip(element, stopClipOptions),
|
||||
setViewportCineClosed: viewportId => service.setViewportCineClosed(viewportId),
|
||||
clearViewportCineClosed: viewportId => service.clearViewportCineClosed(viewportId),
|
||||
isViewportCineClosed: viewportId => service.isViewportCineClosed(viewportId),
|
||||
};
|
||||
|
||||
return <CineContext.Provider value={[state, api]}>{children}</CineContext.Provider>;
|
||||
}
|
||||
|
||||
export const useCine = () => useContext(CineContext);
|
||||
@ -3,9 +3,11 @@ import { ViewportGridContext, ViewportGridProvider, useViewportGrid } from './Vi
|
||||
import { ModalProvider, useModal } from './ModalProvider';
|
||||
import { DialogProvider, useDialog } from './DialogProvider';
|
||||
import ManagedDialog from './ManagedDialog';
|
||||
import CineProvider, { useCine } from './CineProvider';
|
||||
|
||||
export { useNotification, NotificationProvider };
|
||||
export { ViewportGridContext, ViewportGridProvider, useViewportGrid };
|
||||
export { ModalProvider, useModal };
|
||||
export { DialogProvider, useDialog };
|
||||
export { ManagedDialog };
|
||||
export { CineProvider, useCine };
|
||||
Loading…
Reference in New Issue
Block a user