OHIF-323: Cine Dialog (#1961)
* ohif-323: add cineplayer flow * ohif-323: improve toolbar flow of cine * ohif-323: improve cine styles * ohif-323: use toggle instead of set * ohif-323: add compatibility comment * ohif-323: move cine logic to command * ohif-323: cr update * ohif-323: remove typo and update svg title * ohif-323: remove log * ohif-323: update props and remove dead code Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
This commit is contained in:
1 parent
3e944780dc
commit
760e2a62ae
31 files changed
+575
-70
No files matched your search
@@ -0,0 +1,9 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 448 512"
|
||||
aria-labelledby="title"
|
||||
fill="currentColor"
|
||||
>
|
||||
<title id="title">Play</title>
|
||||
<path d="M424.4 214.7L72.4 6.6C43.8-10.3 0 6.1 0 47.9V464c0 37.5 40.7 60.1 72.4 41.3l352-208c31.4-18.5 31.5-64.1 0-82.6z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 283 B |
@@ -0,0 +1,9 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 448 512"
|
||||
aria-labelledby="title"
|
||||
fill="currentColor"
|
||||
>
|
||||
<title id="title">Stop</title>
|
||||
<path d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 277 B |
@@ -0,0 +1,97 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import debounce from 'lodash.debounce';
|
||||
import { IconButton, Icon } from '@ohif/ui';
|
||||
|
||||
import './CinePlayerCustomInputRange.css';
|
||||
|
||||
const CinePlayer = ({
|
||||
isPlaying,
|
||||
minFrameRate,
|
||||
maxFrameRate,
|
||||
stepFrameRate,
|
||||
frameRate: defaultFrameRate,
|
||||
onFrameRateChange,
|
||||
onPlayPauseChange,
|
||||
onClose
|
||||
}) => {
|
||||
const [frameRate, setFrameRate] = useState(defaultFrameRate);
|
||||
const debouncedSetFrameRate = debounce(onFrameRateChange, 300);
|
||||
|
||||
const onFrameRateChangeHandler = ({ target }) => {
|
||||
const frameRate = parseFloat(target.value);
|
||||
debouncedSetFrameRate(frameRate);
|
||||
setFrameRate(frameRate);
|
||||
};
|
||||
|
||||
const onPlayPauseChangeHandler = () => onPlayPauseChange(!isPlaying);
|
||||
|
||||
const action = {
|
||||
false: { icon: 'old-play' },
|
||||
true: { icon: 'old-stop' }
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="CinePlayer flex flex-row h-10 items-center justify-center border border-primary-light rounded-full">
|
||||
<IconButton
|
||||
variant="text"
|
||||
color="inherit"
|
||||
size="initial"
|
||||
className="mr-3 ml-4 text-primary-active"
|
||||
onClick={onPlayPauseChangeHandler}
|
||||
>
|
||||
<Icon width="15px" height="15px" name={action[isPlaying].icon} />
|
||||
</IconButton>
|
||||
<div className="flex flex-col h-full justify-center pt-2 mr-3 pl-1 pr-1">
|
||||
<input
|
||||
type="range"
|
||||
name="frameRate"
|
||||
min={minFrameRate}
|
||||
max={maxFrameRate}
|
||||
step={stepFrameRate}
|
||||
value={frameRate}
|
||||
onChange={onFrameRateChangeHandler}
|
||||
/>
|
||||
<p className="mt-1 text-sm text-primary-light">{`${frameRate.toFixed(1)} fps`}</p>
|
||||
</div>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="initial"
|
||||
className="mr-3 text-primary-active border border-primary-active rounded-full"
|
||||
onClick={onClose}
|
||||
>
|
||||
<Icon name="close" width="15px" height="15px" />
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const noop = () => { };
|
||||
|
||||
CinePlayer.defaultProps = {
|
||||
isPlaying: false,
|
||||
minFrameRate: 1,
|
||||
maxFrameRate: 90,
|
||||
stepFrameRate: 1,
|
||||
frameRate: 24,
|
||||
onPlayPauseChange: noop,
|
||||
onFrameRateChange: noop,
|
||||
onClose: noop
|
||||
};
|
||||
|
||||
CinePlayer.propTypes = {
|
||||
/** Minimum value for range slider */
|
||||
minFrameRate: PropTypes.number.isRequired,
|
||||
/** Maximum value for range slider */
|
||||
maxFrameRate: PropTypes.number.isRequired,
|
||||
/** Increment range slider can "step" in either direction */
|
||||
stepFrameRate: PropTypes.number.isRequired,
|
||||
frameRate: PropTypes.number.isRequired,
|
||||
/** 'true' if playing, 'false' if paused */
|
||||
isPlaying: PropTypes.bool.isRequired,
|
||||
onPlayPauseChange: PropTypes.func,
|
||||
onFrameRateChange: PropTypes.func,
|
||||
onClose: PropTypes.func,
|
||||
};
|
||||
|
||||
export default CinePlayer;
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: CinePlayer
|
||||
menu: General
|
||||
route: components/CinePlayer
|
||||
---
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Playground, Props } from 'docz';
|
||||
import { CinePlayer } from '@ohif/ui';
|
||||
|
||||
# Input Text
|
||||
|
||||
## Import
|
||||
|
||||
```javascript
|
||||
import { CinePlayer } from '@ohif/ui';
|
||||
```
|
||||
|
||||
## Basic usage
|
||||
|
||||
<Playground>
|
||||
{() => {
|
||||
return (
|
||||
<div className="flex flex-col flex-1 p-4 items-center">
|
||||
<div className="w-56">
|
||||
<CinePlayer
|
||||
cineMinFrameRate={1}
|
||||
cineMaxFrameRate={90}
|
||||
cineStepFrameRate={1}
|
||||
cineFrameRate={24}
|
||||
isPlaying={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
|
||||
</Playground>
|
||||
|
||||
## Properties
|
||||
|
||||
<Props of={CinePlayer} />
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* This is a custom input style scoped specifically to CinePlayer
|
||||
* written in plain CSS with color variables from tailwind
|
||||
* to avoid complex compatibility configuration.
|
||||
*/
|
||||
.CinePlayer input[type=range] {
|
||||
-webkit-appearance: none;
|
||||
background: transparent;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.CinePlayer input[type=range]:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.CinePlayer input[type=range]::-webkit-slider-runnable-track {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
cursor: pointer;
|
||||
animate: 0.2s;
|
||||
box-shadow: 0px 0px 0px #000000;
|
||||
@apply bg-primary-light;
|
||||
border-radius: 5px;
|
||||
border: 0px solid #000000;
|
||||
}
|
||||
|
||||
.CinePlayer input[type=range]::-webkit-slider-thumb {
|
||||
box-shadow: 0px 0px 0px #000000;
|
||||
border: 4px solid #000000;
|
||||
height: 18px;
|
||||
width: 17px;
|
||||
border-radius: 50px;
|
||||
@apply bg-primary-light;
|
||||
cursor: pointer;
|
||||
-webkit-appearance: none;
|
||||
margin-top: -9px;
|
||||
}
|
||||
|
||||
.CinePlayer input[type=range]:focus::-webkit-slider-runnable-track {
|
||||
@apply bg-primary-light;
|
||||
}
|
||||
|
||||
.CinePlayer input[type=range]::-moz-range-track {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
cursor: pointer;
|
||||
animate: 0.2s;
|
||||
box-shadow: 0px 0px 0px #000000;
|
||||
@apply bg-primary-light;
|
||||
border-radius: 5px;
|
||||
border: 0px solid #000000;
|
||||
}
|
||||
|
||||
.CinePlayer input[type=range]::-moz-range-thumb {
|
||||
box-shadow: 0px 0px 0px #000000;
|
||||
border: 4px solid #000000;
|
||||
height: 18px;
|
||||
width: 17px;
|
||||
border-radius: 50px;
|
||||
@apply bg-primary-light;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.CinePlayer input[type=range]::-ms-track {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
cursor: pointer;
|
||||
animate: 0.2s;
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.CinePlayer input[type=range]::-ms-fill-lower {
|
||||
@apply bg-primary-light;
|
||||
border: 0px solid #000000;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0px 0px 0px #000000;
|
||||
}
|
||||
|
||||
.CinePlayer input[type=range]::-ms-fill-upper {
|
||||
@apply bg-primary-light;
|
||||
border: 0px solid #000000;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0px 0px 0px #000000;
|
||||
}
|
||||
|
||||
.CinePlayer input[type=range]::-ms-thumb {
|
||||
margin-top: 1px;
|
||||
box-shadow: 0px 0px 0px #000000;
|
||||
border: 4px solid #000000;
|
||||
height: 18px;
|
||||
width: 17px;
|
||||
border-radius: 50px;
|
||||
@apply bg-primary-light;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.CinePlayer input[type=range]:focus::-ms-fill-lower {
|
||||
@apply bg-primary-light;
|
||||
}
|
||||
|
||||
.CinePlayer input[type=range]:focus::-ms-fill-upper {
|
||||
@apply bg-primary-light;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import CinePlayer from './CinePlayer';
|
||||
export default CinePlayer;
|
||||
@@ -60,6 +60,8 @@ import oldAngleLeft from './../../assets/icons/old-angle-left.svg';
|
||||
import oldReset from './../../assets/icons/old-reset.svg';
|
||||
import oldCircleO from './../../assets/icons/old-circle-o.svg';
|
||||
import oldTrash from './../../assets/icons/old-trash.svg';
|
||||
import oldPlay from './../../assets/icons/old-play.svg';
|
||||
import oldStop from './../../assets/icons/old-stop.svg';
|
||||
|
||||
const ICONS = {
|
||||
'arrow-down': arrowDown,
|
||||
@@ -121,6 +123,8 @@ const ICONS = {
|
||||
'old-reset': oldReset,
|
||||
'old-circle-o': oldCircleO,
|
||||
'old-trash': oldTrash,
|
||||
'old-play': oldPlay,
|
||||
'old-stop': oldStop,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { Icon, ButtonGroup, Button, Tooltip } from '../';
|
||||
import { Icon, ButtonGroup, Button, Tooltip, CinePlayer } from '../';
|
||||
import useOnClickOutside from '../../utils/useOnClickOutside';
|
||||
|
||||
const classes = {
|
||||
@@ -14,6 +14,8 @@ const classes = {
|
||||
const ViewportActionBar = ({
|
||||
studyData,
|
||||
showNavArrows,
|
||||
showCine,
|
||||
cineProps,
|
||||
showPatientInfo: patientInfoVisibility,
|
||||
onSeriesChange,
|
||||
onDoubleClick,
|
||||
@@ -71,11 +73,11 @@ const ViewportActionBar = ({
|
||||
if (modality === 'SR') {
|
||||
const TooltipMessage = isLocked
|
||||
? () => (
|
||||
<div>
|
||||
This SR is locked. <br />
|
||||
<div>
|
||||
This SR is locked. <br />
|
||||
Measurements cannot be duplicated.
|
||||
</div>
|
||||
)
|
||||
</div>
|
||||
)
|
||||
: () => <div>This SR is unlocked.</div>;
|
||||
return (
|
||||
<>
|
||||
@@ -117,13 +119,13 @@ const ViewportActionBar = ({
|
||||
can be viewed <br /> in the measurement panel
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Measurements for
|
||||
<>
|
||||
Measurements for
|
||||
<span className="font-bold text-white"> untracked </span>
|
||||
series <br /> will not be shown in the <br /> measurements
|
||||
panel
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -137,10 +139,10 @@ const ViewportActionBar = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center p-2 border-b select-none border-primary-light min-h-12"
|
||||
className="flex flex-wrap items-center p-2 border-b select-none border-primary-light -mt-2"
|
||||
onDoubleClick={onDoubleClick}
|
||||
>
|
||||
<div className="flex flex-grow">
|
||||
<div className="flex flex-grow min-w-48 flex-1 mt-2">
|
||||
<div className="flex items-center">
|
||||
{renderIconStatus()}
|
||||
<span className="ml-2 text-white text-large">{label}</span>
|
||||
@@ -166,8 +168,8 @@ const ViewportActionBar = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showNavArrows && (
|
||||
<div className="ml-2">
|
||||
{showNavArrows && !showCine && (
|
||||
<div className="mt-2">
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
size="initial"
|
||||
@@ -186,7 +188,12 @@ const ViewportActionBar = ({
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex ml-4 mr-2" onClick={onPatientInfoClick}>
|
||||
{showCine && !showNavArrows && (
|
||||
<div className="mt-2 min-w-48 max-w-48 mr-auto">
|
||||
<CinePlayer {...cineProps} />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex h-8 ml-4 mr-2 mt-2" onClick={onPatientInfoClick}>
|
||||
<PatientInfo
|
||||
showPatientInfoRef={showPatientInfoRef}
|
||||
isOpen={showPatientInfo}
|
||||
@@ -206,6 +213,8 @@ const ViewportActionBar = ({
|
||||
ViewportActionBar.propTypes = {
|
||||
onSeriesChange: PropTypes.func.isRequired,
|
||||
showNavArrows: PropTypes.bool,
|
||||
showCine: PropTypes.bool,
|
||||
cineProps: PropTypes.object,
|
||||
showPatientInfo: PropTypes.bool,
|
||||
studyData: PropTypes.shape({
|
||||
label: PropTypes.string.isRequired,
|
||||
@@ -228,6 +237,8 @@ ViewportActionBar.propTypes = {
|
||||
};
|
||||
|
||||
ViewportActionBar.defaultProps = {
|
||||
cineProps: {},
|
||||
showCine: false,
|
||||
showNavArrows: true,
|
||||
showPatientInfo: false,
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import AboutModal from './AboutModal';
|
||||
import Button from './Button';
|
||||
import ButtonGroup from './ButtonGroup';
|
||||
import ContextMenu from './ContextMenu';
|
||||
import CinePlayer from './CinePlayer';
|
||||
import DateRange from './DateRange';
|
||||
import Dialog from './Dialog';
|
||||
import Dropdown from './Dropdown';
|
||||
@@ -68,6 +69,7 @@ export {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
ContextMenu,
|
||||
CinePlayer,
|
||||
DateRange,
|
||||
Dialog,
|
||||
Dropdown,
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useReducer,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
isCineEnabled: false,
|
||||
cines: {
|
||||
/*
|
||||
* 1: { isPlaying: false, frameRate: 24 };
|
||||
*/
|
||||
}
|
||||
};
|
||||
|
||||
const DEFAULT_CINE = { isPlaying: false, frameRate: 24 };
|
||||
|
||||
export const CineContext = createContext(DEFAULT_STATE);
|
||||
|
||||
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;
|
||||
|
||||
if (!cines[id]) cines[id] = { id, ...DEFAULT_CINE };
|
||||
cines[id].frameRate = frameRate || cines[id].frameRate;
|
||||
cines[id].isPlaying = isPlaying !== undefined ? isPlaying : cines[id].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,
|
||||
};
|
||||
|
||||
return (
|
||||
<CineContext.Provider value={[state, api]}>
|
||||
{children}
|
||||
</CineContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
CineProvider.propTypes = {
|
||||
children: PropTypes.any,
|
||||
service: PropTypes.shape({
|
||||
setServiceImplementation: PropTypes.func,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export const useCine = () => useContext(CineContext);
|
||||
@@ -11,9 +11,11 @@ const DEFAULT_STATE = {
|
||||
numRows: 1,
|
||||
numCols: 1,
|
||||
viewports: [
|
||||
// {
|
||||
// displaySetInstanceUID: string,
|
||||
// }
|
||||
/*
|
||||
* {
|
||||
* displaySetInstanceUID: string,
|
||||
* }
|
||||
*/
|
||||
],
|
||||
activeViewportIndex: 0,
|
||||
};
|
||||
@@ -59,7 +61,9 @@ export function ViewportGridProvider({ children, service }) {
|
||||
numCols: 1,
|
||||
numRows: 1,
|
||||
activeViewportIndex: 0,
|
||||
viewports: [{ displaySetInstanceUID: null }],
|
||||
viewports: [{
|
||||
displaySetInstanceUID: null,
|
||||
}],
|
||||
cachedLayout: null,
|
||||
};
|
||||
}
|
||||
@@ -84,13 +88,13 @@ export function ViewportGridProvider({ children, service }) {
|
||||
DEFAULT_STATE
|
||||
);
|
||||
|
||||
console.log('viewportGridState',viewportGridState)
|
||||
|
||||
const getState = useCallback(() => viewportGridState, [viewportGridState]);
|
||||
|
||||
const setActiveViewportIndex = useCallback(
|
||||
index => dispatch({ type: 'SET_ACTIVE_VIEWPORT_INDEX', payload: index }),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const setDisplaysetForViewport = useCallback(
|
||||
({ viewportIndex, displaySetInstanceUID }) =>
|
||||
dispatch({
|
||||
@@ -123,6 +127,7 @@ export function ViewportGridProvider({ children, service }) {
|
||||
}),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const setCachedLayout = useCallback(
|
||||
payload =>
|
||||
dispatch({
|
||||
|
||||
@@ -19,6 +19,12 @@ export {
|
||||
useImageViewer,
|
||||
} from './ImageViewerProvider';
|
||||
|
||||
export {
|
||||
CineContext,
|
||||
default as CineProvider,
|
||||
useCine,
|
||||
} from './CineProvider';
|
||||
|
||||
export {
|
||||
default as SnackbarProvider,
|
||||
useSnackbar,
|
||||
|
||||
Reference in new issue
Block a user