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:
Igor OctavianoandDanny Brown authored and GitHub committed 2020-08-19 15:24:07 -04:00
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
View File
@@ -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,