feat(ViewportActionBar and CinePlayer): Add new design for action bar and cine player (#3204)
* feat(ViewportActionBar): OHIF issue #3123 (#3186) * feat(ViewportActionBar): OHIF issue #3123 - Renamed previous viewport action bar to be LegacyViewportActionBar - Components LegacyViewportActionBar depends on also renamed: LegacyCinePlayer and LegacyPatientInfo - New Viewport coded to specs in issue - added React hook useResizeObserver - added some tailwind classes * Updated tailwind to 3.2.7. Put external imports like React at the top of the import list. * feat(CinePlayer and ViewportActionBar) (#3198) * feat(CinePlayer and ViewportActionBar) - OHIF issue 3123 - new look cine control implemented - new custom blue color in tailwind config for various hover backgrounds in the cine control - new icons added for cine - Tooltip component now can be placed top (center) on hover - Tooltip component border colour now consistent with specs - fixed NPE in ViewportActionBar - upgraded tailwind to 3.2.7 in platform/ui - fixed issues in various button components brought by tailwind 3.2.7 where classes now need important flag - fixed issue with InputRange component so that the tracked value property can change externally - InputRange component can now optionally show its label - added new measurement tracking state service to hydrate SR without prompting - segmentation can also now be hydrated without prompting * PR feedback: - cine centralized to OHIFCornerstoneViewport - introduced a type for the CinePlayer properties * Addressed PR comments and concerns... The DOM ref for the root component of the ViewportActionBar is now added to state so that the various callbacks and ResizeObserver are updated with it. The CinePlayer FPS slider tooltip was moved up so that its arrow does not intersect the FPS text. The hover area for the CinePlayer slider tooltip is now the FPS < > buttons and text. The tracked measurements are now filtered to only include those of the active viewport series when the tracked measurement navigation arrows are used. * Addressed PR comments... The update to tailwind 3.2.7 caused several look-and-feel, UI regressions, so we are rolling back to 3.2.4.
@ -1,19 +1,13 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import OHIF, { utils } from '@ohif/core';
|
||||
import {
|
||||
Notification,
|
||||
ViewportActionBar,
|
||||
useViewportGrid,
|
||||
useViewportDialog,
|
||||
LoadingIndicatorProgress,
|
||||
LoadingIndicatorProgress, Notification, useViewportDialog, useViewportGrid, ViewportActionBar
|
||||
} from '@ohif/ui';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
|
||||
import _hydrateSEGDisplaySet from '../utils/_hydrateSEG';
|
||||
import promptHydrateSEG from '../utils/promptHydrateSEG';
|
||||
import hydrateSEGDisplaySet from '../utils/_hydrateSEG';
|
||||
import _getStatusComponent from './_getStatusComponent';
|
||||
|
||||
const { formatDate } = utils;
|
||||
@ -307,16 +301,15 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
SeriesNumber,
|
||||
} = referencedDisplaySetRef.current.metadata;
|
||||
|
||||
const onPillClick = () => {
|
||||
promptHydrateSEG({
|
||||
servicesManager,
|
||||
viewportIndex,
|
||||
const onStatusClick = async () => {
|
||||
const isHydrated = await hydrateSEGDisplaySet({
|
||||
segDisplaySet,
|
||||
}).then(isHydrated => {
|
||||
if (isHydrated) {
|
||||
setIsHydrated(true);
|
||||
}
|
||||
viewportIndex,
|
||||
toolGroupId,
|
||||
servicesManager,
|
||||
});
|
||||
|
||||
setIsHydrated(isHydrated);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -330,14 +323,13 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
getStatusComponent={() => {
|
||||
return _getStatusComponent({
|
||||
isHydrated,
|
||||
onPillClick,
|
||||
onStatusClick,
|
||||
});
|
||||
}}
|
||||
studyData={{
|
||||
label: viewportLabel,
|
||||
useAltStyling: true,
|
||||
studyDate: formatDate(StudyDate),
|
||||
currentSeries: SeriesNumber,
|
||||
seriesDescription: `SEG Viewport ${SeriesDescription}`,
|
||||
patientInformation: {
|
||||
patientName: PatientName
|
||||
|
||||
@ -1,92 +1,56 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Icon, Tooltip } from '@ohif/ui';
|
||||
|
||||
import _hydrateSEGDisplaySet from '../utils/_hydrateSEG';
|
||||
|
||||
export default function _getStatusComponent({ isHydrated, onPillClick }) {
|
||||
export default function _getStatusComponent({ isHydrated, onStatusClick }) {
|
||||
let ToolTipMessage = null;
|
||||
let StatusIcon = null;
|
||||
|
||||
const {t} = useTranslation("Common");
|
||||
const loadStr = t("LOAD");
|
||||
|
||||
switch (isHydrated) {
|
||||
case true:
|
||||
StatusIcon = () => (
|
||||
<div
|
||||
className="flex items-center justify-center -mr-1 rounded-full"
|
||||
style={{
|
||||
width: '18px',
|
||||
height: '18px',
|
||||
backgroundColor: '#98e5c1',
|
||||
border: 'solid 1.5px #000000',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="exclamation"
|
||||
style={{ color: '#000', width: '12px', height: '12px' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
StatusIcon = () => <Icon name="status-alert" />;
|
||||
|
||||
ToolTipMessage = () => (
|
||||
<div>This Segmentation is loaded in the segmentation panel</div>
|
||||
);
|
||||
break;
|
||||
case false:
|
||||
StatusIcon = () => (
|
||||
<div
|
||||
className="flex items-center justify-center -mr-1 bg-white rounded-full group-hover:bg-customblue-200"
|
||||
style={{
|
||||
width: '18px',
|
||||
height: '18px',
|
||||
border: 'solid 1.5px #000000',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="arrow-left"
|
||||
style={{ color: '#000', width: '14px', height: '14px' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case false:
|
||||
StatusIcon = () => <Icon name="status-untracked" />;
|
||||
|
||||
ToolTipMessage = () => <div>Click to load segmentation.</div>;
|
||||
ToolTipMessage = () => <div>Click LOAD to load segmentation.</div>;
|
||||
}
|
||||
|
||||
const StatusPill = () => (
|
||||
<div
|
||||
className={classNames(
|
||||
'group relative flex items-center justify-center px-8 rounded-full cursor-default bg-customgreen-100',
|
||||
{
|
||||
'hover:bg-customblue-100': !isHydrated,
|
||||
'cursor-pointer': !isHydrated,
|
||||
}
|
||||
)}
|
||||
style={{
|
||||
height: '24px',
|
||||
width: '55px',
|
||||
}}
|
||||
onClick={() => {
|
||||
if (!isHydrated) {
|
||||
if (onPillClick) {
|
||||
onPillClick();
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="pr-1 text-base font-medium leading-none text-black">
|
||||
SEG
|
||||
const StatusArea = () => (
|
||||
<div className="flex h-6 leading-6 cursor-default text-sm text-white">
|
||||
<div className="min-w-[45px] flex items-center p-1 rounded-l-xl rounded-r bg-customgray-100">
|
||||
<StatusIcon />
|
||||
<span className="ml-1">SEG</span>
|
||||
</div>
|
||||
<StatusIcon />
|
||||
{!isHydrated && (
|
||||
<div
|
||||
className="ml-1 px-1.5 rounded cursor-pointer hover:text-black bg-primary-main hover:bg-primary-light"
|
||||
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
|
||||
onMouseUp={onStatusClick}
|
||||
>
|
||||
{loadStr}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{ToolTipMessage && (
|
||||
<Tooltip content={<ToolTipMessage />} position="bottom-left">
|
||||
<StatusPill />
|
||||
<StatusArea />
|
||||
</Tooltip>
|
||||
)}
|
||||
{!ToolTipMessage && <StatusPill />}
|
||||
{!ToolTipMessage && <StatusArea />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,18 +1,17 @@
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import OHIF, { utils } from '@ohif/core';
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import OHIF, { utils } from '@ohif/core';
|
||||
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
|
||||
|
||||
import {
|
||||
Notification,
|
||||
ViewportActionBar,
|
||||
useViewportGrid,
|
||||
useViewportDialog,
|
||||
Tooltip,
|
||||
Icon,
|
||||
Notification,
|
||||
Tooltip,
|
||||
useViewportDialog,
|
||||
useViewportGrid,
|
||||
ViewportActionBar,
|
||||
} from '@ohif/ui';
|
||||
import classNames from 'classnames';
|
||||
import hydrateStructuredReport from '../utils/hydrateStructuredReport';
|
||||
|
||||
const { formatDate } = utils;
|
||||
@ -33,8 +32,6 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
extensionManager,
|
||||
} = props;
|
||||
|
||||
const { t } = useTranslation('SRViewport');
|
||||
|
||||
const {
|
||||
displaySetService,
|
||||
cornerstoneViewportService,
|
||||
@ -379,7 +376,6 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
label: viewportLabel,
|
||||
useAltStyling: true,
|
||||
studyDate: formatDate(StudyDate),
|
||||
currentSeries: SeriesNumber,
|
||||
seriesDescription: SeriesDescription || '',
|
||||
patientInformation: {
|
||||
patientName: PatientName
|
||||
@ -468,13 +464,16 @@ function _getStatusComponent({
|
||||
isLocked,
|
||||
sendTrackedMeasurementsEvent,
|
||||
}) {
|
||||
const onPillClick = () => {
|
||||
sendTrackedMeasurementsEvent('RESTORE_PROMPT_HYDRATE_SR', {
|
||||
const handleMouseUp = () => {
|
||||
sendTrackedMeasurementsEvent('HYDRATE_SR', {
|
||||
displaySetInstanceUID: srDisplaySet.displaySetInstanceUID,
|
||||
viewportIndex,
|
||||
});
|
||||
};
|
||||
|
||||
const { t } = useTranslation('Common');
|
||||
const loadStr = t('LOAD');
|
||||
|
||||
// 1 - Incompatible
|
||||
// 2 - Locked
|
||||
// 3 - Rehydratable / Open
|
||||
@ -485,22 +484,7 @@ function _getStatusComponent({
|
||||
|
||||
switch (state) {
|
||||
case 1:
|
||||
StatusIcon = () => (
|
||||
<div
|
||||
className="flex items-center justify-center -mr-1 rounded-full"
|
||||
style={{
|
||||
width: '18px',
|
||||
height: '18px',
|
||||
backgroundColor: '#98e5c1',
|
||||
border: 'solid 1.5px #000000',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="exclamation"
|
||||
style={{ color: '#000', width: '12px', height: '12px' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
StatusIcon = () => <Icon name="status-alert" />;
|
||||
|
||||
ToolTipMessage = () => (
|
||||
<div>
|
||||
@ -511,20 +495,7 @@ function _getStatusComponent({
|
||||
);
|
||||
break;
|
||||
case 2:
|
||||
StatusIcon = () => (
|
||||
<div
|
||||
className="flex items-center justify-center -mr-1 bg-black rounded-full"
|
||||
style={{
|
||||
width: '18px',
|
||||
height: '18px',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="lock"
|
||||
style={{ color: '#05D97C', width: '8px', height: '11px' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
StatusIcon = () => <Icon name="status-locked" />;
|
||||
|
||||
ToolTipMessage = () => (
|
||||
<div>
|
||||
@ -537,48 +508,28 @@ function _getStatusComponent({
|
||||
);
|
||||
break;
|
||||
case 3:
|
||||
StatusIcon = () => (
|
||||
<div
|
||||
className="flex items-center justify-center -mr-1 bg-white rounded-full group-hover:bg-customblue-200"
|
||||
style={{
|
||||
width: '18px',
|
||||
height: '18px',
|
||||
border: 'solid 1.5px #000000',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="arrow-left"
|
||||
style={{ color: '#000', width: '14px', height: '14px' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
StatusIcon = () => <Icon name="status-untracked" />;
|
||||
|
||||
ToolTipMessage = () => <div>Click to restore measurements.</div>;
|
||||
ToolTipMessage = () => (
|
||||
<div>{`Click ${loadStr} to restore measurements.`}</div>
|
||||
);
|
||||
}
|
||||
|
||||
const StatusPill = () => (
|
||||
<div
|
||||
className={classNames(
|
||||
'group relative flex items-center justify-center px-2 rounded-full cursor-default bg-customgreen-100',
|
||||
{
|
||||
'hover:bg-customblue-100': state === 3,
|
||||
'cursor-pointer': state === 3,
|
||||
}
|
||||
const StatusArea = () => (
|
||||
<div className="flex h-6 leading-6 cursor-default text-sm text-white">
|
||||
<div className="min-w-[45px] flex items-center p-1 rounded-l-xl rounded-r bg-customgray-100">
|
||||
<StatusIcon />
|
||||
<span className="ml-1">SR</span>
|
||||
</div>
|
||||
{state === 3 && (
|
||||
<div
|
||||
className="ml-1 px-1.5 rounded cursor-pointer hover:text-black bg-primary-main hover:bg-primary-light"
|
||||
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
|
||||
onMouseUp={handleMouseUp}
|
||||
>
|
||||
{loadStr}
|
||||
</div>
|
||||
)}
|
||||
style={{
|
||||
height: '24px',
|
||||
width: '55px',
|
||||
}}
|
||||
onClick={() => {
|
||||
if (state === 3) {
|
||||
if (onPillClick) {
|
||||
onPillClick();
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="pr-1 text-lg font-bold leading-none text-black">SR</span>
|
||||
<StatusIcon />
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -586,22 +537,12 @@ function _getStatusComponent({
|
||||
<>
|
||||
{ToolTipMessage && (
|
||||
<Tooltip content={<ToolTipMessage />} position="bottom-left">
|
||||
<StatusPill />
|
||||
<StatusArea />
|
||||
</Tooltip>
|
||||
)}
|
||||
{!ToolTipMessage && <StatusPill />}
|
||||
{!ToolTipMessage && <StatusArea />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// function _onDoubleClick() {
|
||||
// const cancelActiveManipulatorsForElement = cornerstoneTools.getModule(
|
||||
// 'manipulatorState'
|
||||
// ).setters.cancelActiveManipulatorsForElement;
|
||||
// const enabledElements = cornerstoneTools.store.state.enabledElements;
|
||||
// enabledElements.forEach(element => {
|
||||
// cancelActiveManipulatorsForElement(element);
|
||||
// });
|
||||
// }
|
||||
|
||||
export default OHIFCornerstoneSRViewport;
|
||||
|
||||
@ -20,6 +20,7 @@ import {
|
||||
IVolumeViewport,
|
||||
} from '@cornerstonejs/core/dist/esm/types';
|
||||
import getSOPInstanceAttributes from '../utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
|
||||
import { CinePlayer, useCine, useViewportGrid } from '@ohif/ui';
|
||||
|
||||
const STACK = 'stack';
|
||||
|
||||
@ -112,6 +113,9 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
} = props;
|
||||
|
||||
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
|
||||
const [{ isCineEnabled, cines }, cineService] = useCine();
|
||||
const [{ activeViewportIndex }] = useViewportGrid();
|
||||
const [enabledVPElement, setEnabledVPElement] = useState(null);
|
||||
|
||||
const elementRef = useRef();
|
||||
|
||||
@ -126,6 +130,73 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
viewportGridService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const cineHandler = () => {
|
||||
if (!cines || !cines[viewportIndex] || !enabledVPElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cine = cines[viewportIndex];
|
||||
const isPlaying = cine.isPlaying || false;
|
||||
const frameRate = cine.frameRate || 24;
|
||||
|
||||
const validFrameRate = Math.max(frameRate, 1);
|
||||
|
||||
if (isPlaying) {
|
||||
cineService.playClip(enabledVPElement, {
|
||||
framesPerSecond: validFrameRate,
|
||||
});
|
||||
} else {
|
||||
cineService.stopClip(enabledVPElement);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
eventTarget.addEventListener(
|
||||
Enums.Events.STACK_VIEWPORT_NEW_STACK,
|
||||
cineHandler
|
||||
);
|
||||
|
||||
return () => {
|
||||
cineService.setCine({ id: viewportIndex, isPlaying: false });
|
||||
eventTarget.removeEventListener(
|
||||
Enums.Events.STACK_VIEWPORT_NEW_STACK,
|
||||
cineHandler
|
||||
);
|
||||
};
|
||||
}, [enabledVPElement]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!cines || !cines[viewportIndex] || !enabledVPElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
cineHandler();
|
||||
|
||||
return () => {
|
||||
if (enabledVPElement && cines?.[viewportIndex]?.isPlaying) {
|
||||
cineService.stopClip(enabledVPElement);
|
||||
}
|
||||
};
|
||||
}, [cines, viewportIndex, cineService, enabledVPElement, cineHandler]);
|
||||
|
||||
const cine = cines[viewportIndex];
|
||||
const isPlaying = (cine && cine.isPlaying) || false;
|
||||
|
||||
const handleCineClose = () => {
|
||||
toolbarService.recordInteraction({
|
||||
groupId: 'MoreTools',
|
||||
itemId: 'cine',
|
||||
interactionType: 'toggle',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'toggleCine',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
// useCallback for scroll bar height calculation
|
||||
const setImageScrollBarHeight = useCallback(() => {
|
||||
const scrollbarHeight = `${elementRef.current.clientHeight - 20}px`;
|
||||
@ -176,6 +247,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
const viewportIndex = viewportInfo.getViewportIndex();
|
||||
|
||||
setEnabledElement(viewportIndex, element);
|
||||
setEnabledVPElement(element);
|
||||
|
||||
const renderingEngineId = viewportInfo.getRenderingEngineId();
|
||||
const toolGroupId = viewportInfo.getToolGroupId();
|
||||
@ -360,6 +432,25 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
scrollbarHeight={scrollbarHeight}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
{isCineEnabled && (
|
||||
<CinePlayer
|
||||
className="absolute left-1/2 -translate-x-1/2 bottom-3"
|
||||
isPlaying={isPlaying}
|
||||
onClose={handleCineClose}
|
||||
onPlayPauseChange={isPlaying =>
|
||||
cineService.setCine({
|
||||
id: activeViewportIndex,
|
||||
isPlaying,
|
||||
})
|
||||
}
|
||||
onFrameRateChange={frameRate =>
|
||||
cineService.setCine({
|
||||
id: activeViewportIndex,
|
||||
frameRate,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}, areEqual);
|
||||
|
||||
@ -129,7 +129,7 @@ function CornerstoneViewportOverlay({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-row">
|
||||
<div className="flex flex-row text-base">
|
||||
<span className="mr-1">W:</span>
|
||||
<span className="ml-1 mr-2 font-light">{windowWidth.toFixed(0)}</span>
|
||||
<span className="mr-1">L:</span>
|
||||
@ -140,7 +140,7 @@ function CornerstoneViewportOverlay({
|
||||
|
||||
if (activeTools.includes('Zoom')) {
|
||||
return (
|
||||
<div className="flex flex-row">
|
||||
<div className="flex flex-row text-base">
|
||||
<span className="mr-1">Zoom:</span>
|
||||
<span className="font-light">{scale.toFixed(2)}x</span>
|
||||
</div>
|
||||
@ -174,7 +174,7 @@ function CornerstoneViewportOverlay({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-row">
|
||||
<div className="flex flex-row text-base">
|
||||
<span className="mr-1">I:</span>
|
||||
<span className="font-light">
|
||||
{instanceNumber !== undefined
|
||||
|
||||
@ -12,6 +12,7 @@ import promptTrackNewSeries from './promptTrackNewSeries';
|
||||
import promptTrackNewStudy from './promptTrackNewStudy';
|
||||
import promptSaveReport from './promptSaveReport';
|
||||
import promptHydrateStructuredReport from './promptHydrateStructuredReport';
|
||||
import hydrateStructuredReport from './hydrateStructuredReport';
|
||||
|
||||
const TrackedMeasurementsContext = React.createContext();
|
||||
TrackedMeasurementsContext.displayName = 'TrackedMeasurementsContext';
|
||||
@ -106,6 +107,10 @@ function TrackedMeasurementsContextProvider(
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
}),
|
||||
hydrateStructuredReport: hydrateStructuredReport.bind(null, {
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
}),
|
||||
});
|
||||
|
||||
// TODO: IMPROVE
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
import { hydrateStructuredReport as baseHydrateStructuredReport } from '@ohif/extension-cornerstone-dicom-sr';
|
||||
|
||||
function hydrateStructuredReport(
|
||||
{ servicesManager, extensionManager },
|
||||
ctx,
|
||||
evt
|
||||
) {
|
||||
const { displaySetService } = servicesManager.services;
|
||||
const { viewportIndex, displaySetInstanceUID } = evt;
|
||||
const srDisplaySet = displaySetService.getDisplaySetByUID(
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const hydrationResult = baseHydrateStructuredReport(
|
||||
{ servicesManager, extensionManager },
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
const StudyInstanceUID = hydrationResult.StudyInstanceUID;
|
||||
const SeriesInstanceUIDs = hydrationResult.SeriesInstanceUIDs;
|
||||
|
||||
resolve({
|
||||
displaySetInstanceUID: evt.displaySetInstanceUID,
|
||||
srSeriesInstanceUID: srDisplaySet.SeriesInstanceUID,
|
||||
viewportIndex,
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUIDs,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default hydrateStructuredReport;
|
||||
@ -1,3 +1,4 @@
|
||||
import { hydrateStructuredReport } from '@ohif/extension-cornerstone-dicom-sr';
|
||||
import { assign } from 'xstate';
|
||||
|
||||
const RESPONSE = {
|
||||
@ -45,6 +46,7 @@ const machineConfiguration = {
|
||||
cond: 'hasNotIgnoredSRSeriesForHydration',
|
||||
},
|
||||
RESTORE_PROMPT_HYDRATE_SR: 'promptHydrateStructuredReport',
|
||||
HYDRATE_SR: 'hydrateStructuredReport',
|
||||
},
|
||||
},
|
||||
promptBeginTracking: {
|
||||
@ -232,6 +234,24 @@ const machineConfiguration = {
|
||||
},
|
||||
},
|
||||
},
|
||||
hydrateStructuredReport: {
|
||||
invoke: {
|
||||
src: 'hydrateStructuredReport',
|
||||
onDone: [
|
||||
{
|
||||
target: 'tracking',
|
||||
actions: [
|
||||
'setTrackedStudyAndMultipleSeries',
|
||||
'jumpToFirstMeasurementInActiveViewport',
|
||||
'setIsDirtyToClean',
|
||||
],
|
||||
},
|
||||
],
|
||||
onError: {
|
||||
target: 'idle',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
strict: true,
|
||||
};
|
||||
|
||||
@ -5,8 +5,6 @@ import OHIF, { utils } from '@ohif/core';
|
||||
import {
|
||||
Notification,
|
||||
ViewportActionBar,
|
||||
useCine,
|
||||
useViewportGrid,
|
||||
useViewportDialog,
|
||||
Tooltip,
|
||||
Icon,
|
||||
@ -14,7 +12,6 @@ import {
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { eventTarget, Enums } from '@cornerstonejs/core';
|
||||
import { annotation } from '@cornerstonejs/tools';
|
||||
import { useTrackedMeasurements } from './../getContextModule';
|
||||
|
||||
@ -43,12 +40,9 @@ function TrackedCornerstoneViewport(props) {
|
||||
const displaySet = displaySets[0];
|
||||
|
||||
const [trackedMeasurements] = useTrackedMeasurements();
|
||||
const [{ activeViewportIndex }] = useViewportGrid();
|
||||
const [{ isCineEnabled, cines }, cineService] = useCine();
|
||||
const [viewportDialogState] = useViewportDialog();
|
||||
const [isTracked, setIsTracked] = useState(false);
|
||||
const [trackedMeasurementUID, setTrackedMeasurementUID] = useState(null);
|
||||
const [element, setElement] = useState(null);
|
||||
|
||||
const { trackedSeries } = trackedMeasurements.context;
|
||||
const viewportId = viewportOptions.viewportId;
|
||||
@ -71,26 +65,6 @@ function TrackedCornerstoneViewport(props) {
|
||||
ManufacturerModelName,
|
||||
} = displaySet.images[0];
|
||||
|
||||
const cineHandler = () => {
|
||||
if (!cines || !cines[viewportIndex] || !element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cine = cines[viewportIndex];
|
||||
const isPlaying = cine.isPlaying || false;
|
||||
const frameRate = cine.frameRate || 24;
|
||||
|
||||
const validFrameRate = Math.max(frameRate, 1);
|
||||
|
||||
if (isPlaying) {
|
||||
cineService.playClip(element, {
|
||||
framesPerSecond: validFrameRate,
|
||||
});
|
||||
} else {
|
||||
cineService.stopClip(element);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isTracked) {
|
||||
annotation.config.style.setViewportToolStyles(viewportId, {
|
||||
@ -119,50 +93,10 @@ function TrackedCornerstoneViewport(props) {
|
||||
};
|
||||
}, [isTracked]);
|
||||
|
||||
// unmount cleanup
|
||||
useEffect(() => {
|
||||
eventTarget.addEventListener(
|
||||
Enums.Events.STACK_VIEWPORT_NEW_STACK,
|
||||
cineHandler
|
||||
);
|
||||
|
||||
return () => {
|
||||
cineService.setCine({ id: viewportIndex, isPlaying: false });
|
||||
eventTarget.removeEventListener(
|
||||
Enums.Events.STACK_VIEWPORT_NEW_STACK,
|
||||
cineHandler
|
||||
);
|
||||
};
|
||||
}, [element]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!cines || !cines[viewportIndex] || !element) {
|
||||
return;
|
||||
}
|
||||
|
||||
cineHandler();
|
||||
|
||||
return () => {
|
||||
if (element && cines?.[viewportIndex]?.isPlaying) {
|
||||
cineService.stopClip(element);
|
||||
}
|
||||
};
|
||||
}, [cines, viewportIndex, cineService, element, cineHandler]);
|
||||
|
||||
if (trackedSeries.includes(SeriesInstanceUID) !== isTracked) {
|
||||
setIsTracked(!isTracked);
|
||||
}
|
||||
|
||||
/**
|
||||
* OnElementEnabled callback which is called after the cornerstoneExtension
|
||||
* has enabled the element. Note: we delegate all the image rendering to
|
||||
* cornerstoneExtension, so we don't need to do anything here regarding
|
||||
* the image rendering, element enabling etc.
|
||||
*/
|
||||
const onElementEnabled = evt => {
|
||||
setElement(evt.detail.element);
|
||||
};
|
||||
|
||||
function switchMeasurement(direction) {
|
||||
const newTrackedMeasurementUID = _getNextMeasurementUID(
|
||||
direction,
|
||||
@ -188,12 +122,9 @@ function TrackedCornerstoneViewport(props) {
|
||||
'@ohif/extension-cornerstone.viewportModule.cornerstone'
|
||||
);
|
||||
|
||||
return <Component {...props} onElementEnabled={onElementEnabled} />;
|
||||
return <Component {...props} />;
|
||||
};
|
||||
|
||||
const cine = cines[viewportIndex];
|
||||
const isPlaying = (cine && cine.isPlaying) || false;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewportActionBar
|
||||
@ -210,9 +141,7 @@ function TrackedCornerstoneViewport(props) {
|
||||
currentSeries: SeriesNumber, // TODO - switch entire currentSeries to be UID based or actual position based
|
||||
seriesDescription: SeriesDescription,
|
||||
patientInformation: {
|
||||
patientName: PatientName
|
||||
? OHIF.utils.formatPN(PatientName)
|
||||
: '',
|
||||
patientName: PatientName ? OHIF.utils.formatPN(PatientName) : '',
|
||||
patientSex: PatientSex || '',
|
||||
patientAge: PatientAge || '',
|
||||
MRN: PatientID || '',
|
||||
@ -226,22 +155,6 @@ function TrackedCornerstoneViewport(props) {
|
||||
scanner: ManufacturerModelName || '',
|
||||
},
|
||||
}}
|
||||
showNavArrows={!isCineEnabled}
|
||||
showCine={isCineEnabled}
|
||||
cineProps={{
|
||||
isPlaying,
|
||||
onClose: () => commandsManager.runCommand('toggleCine'),
|
||||
onPlayPauseChange: isPlaying =>
|
||||
cineService.setCine({
|
||||
id: activeViewportIndex,
|
||||
isPlaying,
|
||||
}),
|
||||
onFrameRateChange: frameRate =>
|
||||
cineService.setCine({
|
||||
id: activeViewportIndex,
|
||||
frameRate,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
{/* TODO: Viewport interface to accept stack or layers of content like this? */}
|
||||
<div className="relative flex flex-row w-full h-full overflow-hidden">
|
||||
@ -281,17 +194,25 @@ function _getNextMeasurementUID(
|
||||
trackedMeasurementId,
|
||||
trackedMeasurements
|
||||
) {
|
||||
const { measurementService } = servicesManager.services;
|
||||
const { measurementService, viewportGridService } = servicesManager.services;
|
||||
const measurements = measurementService.getMeasurements();
|
||||
|
||||
const { activeViewportIndex, viewports } = viewportGridService.getState();
|
||||
const {
|
||||
displaySetInstanceUIDs: activeViewportDisplaySetInstanceUIDs,
|
||||
} = viewports[activeViewportIndex];
|
||||
|
||||
const { trackedSeries } = trackedMeasurements.context;
|
||||
|
||||
// Get the potentially trackable measurements for this series,
|
||||
// Get the potentially trackable measurements for the series of the
|
||||
// active viewport.
|
||||
// The measurements to jump between are the same
|
||||
// regardless if this series is tracked or not.
|
||||
|
||||
const filteredMeasurements = measurements.filter(m =>
|
||||
trackedSeries.includes(m.referenceSeriesUID)
|
||||
const filteredMeasurements = measurements.filter(
|
||||
m =>
|
||||
trackedSeries.includes(m.referenceSeriesUID) &&
|
||||
activeViewportDisplaySetInstanceUIDs.includes(m.displaySetInstanceUID)
|
||||
);
|
||||
|
||||
if (!filteredMeasurements.length) {
|
||||
@ -329,7 +250,7 @@ function _getNextMeasurementUID(
|
||||
}
|
||||
|
||||
function _getStatusComponent(isTracked) {
|
||||
const trackedIcon = isTracked ? 'tracked' : 'dotted-circle';
|
||||
const trackedIcon = isTracked ? 'status-tracked' : 'status-untracked';
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
@ -361,7 +282,7 @@ function _getStatusComponent(isTracked) {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Icon name={trackedIcon} className="w-6 text-primary-light" />
|
||||
<Icon name={trackedIcon} className="text-primary-light" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -53,11 +53,17 @@ module.exports = {
|
||||
|
||||
customgreen: {
|
||||
100: '#05D97C',
|
||||
200: '#0FD97C',
|
||||
},
|
||||
|
||||
customblue: {
|
||||
100: '#c4fdff',
|
||||
200: '#38daff',
|
||||
300: '#1D204D',
|
||||
},
|
||||
|
||||
customgray: {
|
||||
100: '#262943',
|
||||
},
|
||||
|
||||
gray: {
|
||||
@ -338,14 +344,15 @@ module.exports = {
|
||||
full: '100%',
|
||||
screen: '100vh',
|
||||
}),
|
||||
inset: {
|
||||
inset: theme => ({
|
||||
...theme('spacing'),
|
||||
'0': '0',
|
||||
auto: 'auto',
|
||||
full: '100%',
|
||||
viewport: '0.5rem',
|
||||
'1/2': '50%',
|
||||
'viewport-scrollbar': '1.3rem',
|
||||
},
|
||||
}),
|
||||
letterSpacing: {
|
||||
tighter: '-0.05em',
|
||||
tight: '-0.025em',
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
"Close": "Close",
|
||||
"Image": "Image",
|
||||
"Layout": "Layout",
|
||||
"LOAD": "LOAD",
|
||||
"Measurements": "Measurements",
|
||||
"More": "More",
|
||||
"Next": "Next",
|
||||
@ -13,4 +14,4 @@
|
||||
"Show": "Show",
|
||||
"Stop": "Stop",
|
||||
"StudyDate": "Study Date"
|
||||
}
|
||||
}
|
||||
|
||||
6
platform/ui/src/assets/icons/arrow-left-small.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg width="7" height="12" viewBox="0 0 7 12" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path d="M0 0h7v12H0z"/>
|
||||
<path stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" d="M5.757 1.757 1.515 6l4.242 4.243"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 312 B |
6
platform/ui/src/assets/icons/arrow-right-small.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg width="7" height="12" viewBox="0 0 7 12" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path d="M7 0H0v12h7z"/>
|
||||
<path stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" d="M1.243 1.757 5.485 6l-4.242 4.243"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 313 B |
6
platform/ui/src/assets/icons/chevron-next.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path fill="currentColor" fill-rule="nonzero" d="M6.41 2 5 3.41 9.58 8 5 12.59 6.41 14l6-6z"/>
|
||||
<path d="M0 0h16v16H0z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 277 B |
6
platform/ui/src/assets/icons/chevron-prev.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path fill="currentColor" fill-rule="nonzero" d="M9.59 2 11 3.41 6.42 8 11 12.59 9.59 14l-6-6z"/>
|
||||
<path d="M16 0H0v16h16z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 281 B |
3
platform/ui/src/assets/icons/component-slider.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="907" cy="375" r="5" transform="translate(-901 -369)" fill="#5ACCE6" stroke="#090C29" stroke-width="2" fill-rule="evenodd"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 231 B |
8
platform/ui/src/assets/icons/icon-close.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path d="M0 0h24v24H0z"/>
|
||||
<g stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5">
|
||||
<path d="M15.656 8 8 15.656M15.656 15.656 8 8"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 348 B |
6
platform/ui/src/assets/icons/icon-pause.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" fill="currentColor" fill-rule="nonzero"/>
|
||||
<path d="M0 0h24v24H0z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 266 B |
6
platform/ui/src/assets/icons/icon-play.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path d="M0 0h24v24H0z"/>
|
||||
<path d="M6 6.597a.597.597 0 0 1 .864-.534l11.806 5.903a.597.597 0 0 1 0 1.068L6.864 18.937A.597.597 0 0 1 6 18.403V6.597z" fill="currentColor"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 328 B |
10
platform/ui/src/assets/icons/info-action.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path d="M0 0h24v24H0z"/>
|
||||
<g fill="currentColor">
|
||||
<path d="M11.885 4a7.885 7.885 0 1 0 0 15.77 7.885 7.885 0 0 0 0-15.77zm0 1a6.885 6.885 0 1 1 0 13.77 6.885 6.885 0 0 1 0-13.77z"/>
|
||||
<path d="M11.27 10.153c.574 0 1.048.435 1.108.993l.007.122v4.308a.5.5 0 0 1-.992.09l-.008-.09v-4.308a.115.115 0 0 0-.08-.11l-.036-.005h-1.23a.5.5 0 0 1-.09-.992l.09-.008h1.23zM10.962 7.076l.1.006a.808.808 0 1 1-.202 0l.102-.006z"/>
|
||||
<path d="M13.73 15.077a.5.5 0 0 1 .09.992l-.09.008h-3.692a.5.5 0 0 1-.09-.992l.09-.008h3.693z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 716 B |
7
platform/ui/src/assets/icons/status-alert.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path d="M0 0h16v16H0z"/>
|
||||
<rect stroke="#7BB2CE" fill="#0D0E24" x=".5" y=".5" width="15" height="15" rx="7.5"/>
|
||||
<path stroke="#FFF" stroke-linecap="round" stroke-linejoin="round" d="M8.002 8.5v-5M7.989 11.5a.245.245 0 0 0-.237.255.254.254 0 0 0 .25.245h.009a.246.246 0 0 0 .237-.255.253.253 0 0 0-.246-.245"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 474 B |
7
platform/ui/src/assets/icons/status-locked.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<rect x=".5" y=".5" width="15" height="15" rx="7.5" fill="#0D0E24" stroke="#7BB2CE"/>
|
||||
<path d="M5.84 12h4.365c.605 0 1.09-.506 1.09-1.125V7.5c0-.619-.485-1.125-1.09-1.125V5.25c0-1.243-.977-2.25-2.182-2.25C6.817 3 5.84 4.007 5.84 5.25v1.125c-.606 0-1.091.506-1.091 1.125v3.375c0 .619.485 1.125 1.09 1.125zm.874-6.75c0-.748.583-1.35 1.309-1.35.725 0 1.309.608 1.309 1.35v1.125H6.714V5.25z" fill="#FFF" fill-rule="nonzero"/>
|
||||
<path d="M0 0h16v16H0z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 612 B |
7
platform/ui/src/assets/icons/status-tracked.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path d="M0 0h16v16H0z"/>
|
||||
<rect stroke="#5ACCE6" fill="#5ACCE6" x=".5" y=".5" width="15" height="15" rx="7.5"/>
|
||||
<path d="m11.389 5-3.615 5.595a.91.91 0 0 1-.68.43.866.866 0 0 1-.727-.313L4.5 8.545" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 442 B |
6
platform/ui/src/assets/icons/status-untracked.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path d="M0 0h16v16H0z"/>
|
||||
<rect stroke="#7BB2CE" fill="#0D0E24" x=".5" y=".5" width="15" height="15" rx="7.5"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 268 B |
3
platform/ui/src/components/CinePlayer/CinePlayer.css
Normal file
@ -0,0 +1,3 @@
|
||||
.cine-fps-range-tooltip .tooltip.tooltip-top {
|
||||
bottom: 85% !important;
|
||||
}
|
||||
@ -1,11 +1,28 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import debounce from 'lodash.debounce';
|
||||
import { IconButton, Icon } from '../';
|
||||
import { Icon, Tooltip, InputRange } from '../';
|
||||
|
||||
import './CinePlayerCustomInputRange.css';
|
||||
import './CinePlayer.css';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const CinePlayer = ({
|
||||
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;
|
||||
};
|
||||
|
||||
const fpsButtonClassNames =
|
||||
'cursor-pointer text-primary-active active:text-primary-light hover:bg-customblue-300 w-4 flex items-center justify-center';
|
||||
|
||||
const CinePlayer: React.FC<CinePlayerProps> = ({
|
||||
className,
|
||||
isPlaying,
|
||||
minFrameRate,
|
||||
maxFrameRate,
|
||||
@ -18,52 +35,68 @@ const CinePlayer = ({
|
||||
const [frameRate, setFrameRate] = useState(defaultFrameRate);
|
||||
const debouncedSetFrameRate = debounce(onFrameRateChange, 300);
|
||||
|
||||
const onFrameRateChangeHandler = ({ target }) => {
|
||||
const frameRate = parseFloat(target.value);
|
||||
debouncedSetFrameRate(frameRate);
|
||||
const getPlayPauseIconName = () => (isPlaying ? 'icon-pause' : 'icon-play');
|
||||
|
||||
const handleSetFrameRate = (frameRate: number) => {
|
||||
if (frameRate < minFrameRate || frameRate > maxFrameRate) {
|
||||
return;
|
||||
}
|
||||
setFrameRate(frameRate);
|
||||
};
|
||||
|
||||
const onPlayPauseChangeHandler = () => onPlayPauseChange(!isPlaying);
|
||||
|
||||
const action = {
|
||||
false: { icon: 'old-play' },
|
||||
true: { icon: 'old-stop' },
|
||||
debouncedSetFrameRate(frameRate);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-row items-center justify-center h-10 border rounded-full CinePlayer border-primary-light">
|
||||
<IconButton
|
||||
variant="text"
|
||||
color="inherit"
|
||||
size="initial"
|
||||
className="ml-4 mr-3 text-primary-active"
|
||||
onClick={onPlayPauseChangeHandler}
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
'select-none flex items-center gap-2 px-2 py-2 rounded border border-secondary-light/60 bg-primary-dark'
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
name={getPlayPauseIconName()}
|
||||
className="cursor-pointer text-white active:text-primary-light hover:bg-customblue-300 hover:rounded"
|
||||
onClick={() => onPlayPauseChange(!isPlaying)}
|
||||
/>
|
||||
<Tooltip
|
||||
position="top"
|
||||
className="group/fps cine-fps-range-tooltip"
|
||||
tight={true}
|
||||
content={
|
||||
<InputRange
|
||||
containerClassName="h-9 px-2"
|
||||
inputClassName="w-40"
|
||||
value={frameRate}
|
||||
minValue={minFrameRate}
|
||||
maxValue={maxFrameRate}
|
||||
step={stepFrameRate}
|
||||
onChange={handleSetFrameRate}
|
||||
showLabel={false}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Icon width="15px" height="15px" name={action[isPlaying].icon} />
|
||||
</IconButton>
|
||||
<div className="flex flex-col justify-center h-full pt-2 pl-1 pr-1 mr-3">
|
||||
<input
|
||||
type="range"
|
||||
name="frameRate"
|
||||
min={minFrameRate}
|
||||
max={maxFrameRate}
|
||||
step={stepFrameRate}
|
||||
value={frameRate}
|
||||
onChange={onFrameRateChangeHandler}
|
||||
/>
|
||||
<p className="-mt-2 text-sm text-primary-light">{`${frameRate.toFixed(
|
||||
1
|
||||
)} fps`}</p>
|
||||
</div>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="initial"
|
||||
className="mr-3 border rounded-full text-primary-active border-primary-active"
|
||||
<div className="border border-secondary-light flex h-6 items-stretch rounded gap-1">
|
||||
<div
|
||||
className={`${fpsButtonClassNames} rounded-l`}
|
||||
onClick={() => handleSetFrameRate(frameRate - 1)}
|
||||
>
|
||||
<Icon name="arrow-left-small" />
|
||||
</div>
|
||||
<div className="w-11 text-sm text-white text-center group-hover/fps:text-primary-light leading-[22px]">
|
||||
{`${frameRate} FPS`}
|
||||
</div>
|
||||
<div
|
||||
className={`${fpsButtonClassNames} rounded-r`}
|
||||
onClick={() => handleSetFrameRate(frameRate + 1)}
|
||||
>
|
||||
<Icon name="arrow-right-small" />
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Icon
|
||||
name="icon-close"
|
||||
className="cursor-pointer text-primary-active active:text-primary-light hover:bg-customblue-300 hover:rounded"
|
||||
onClick={onClose}
|
||||
>
|
||||
<Icon name="close" width="15px" height="15px" />
|
||||
</IconButton>
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
import CinePlayer from '../CinePlayer';
|
||||
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
|
||||
import { createComponentTemplate } from '../../../storybook/functions/create-component-story';
|
||||
|
||||
export const argTypes = {
|
||||
component: CinePlayer,
|
||||
title: 'Components/CinePlayer',
|
||||
};
|
||||
|
||||
<Meta title="Components/CinePlayer" component={CinePlayer} />
|
||||
|
||||
export const cineTemplate = createComponentTemplate(CinePlayer);
|
||||
|
||||
<Heading title="CinePlayer" componentRelativePath="CinePlayer/CinePlayer.tsx" />
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Props](#props)
|
||||
- [Contribute](#contribute)
|
||||
|
||||
## Overview
|
||||
|
||||
<Canvas>
|
||||
<Story name="Overview">{cineTemplate.bind({})}</Story>
|
||||
</Canvas>
|
||||
|
||||
## Props
|
||||
|
||||
<ArgsTable of={CinePlayer} />
|
||||
|
||||
## Contribute
|
||||
|
||||
<Footer componentRelativePath="CinePlayer/__stories__/cinePlayer.stories.mdx" />
|
||||
@ -3,6 +3,8 @@ import React from 'react';
|
||||
|
||||
import arrowDown from './../../assets/icons/arrow-down.svg';
|
||||
import arrowLeft from './../../assets/icons/arrow-left.svg';
|
||||
import arrowLeftSmall from './../../assets/icons/arrow-left-small.svg';
|
||||
import arrowRightSmall from './../../assets/icons/arrow-right-small.svg';
|
||||
import calendar from './../../assets/icons/calendar.svg';
|
||||
import cancel from './../../assets/icons/cancel.svg';
|
||||
import clipboard from './../../assets/icons/clipboard.svg';
|
||||
@ -11,6 +13,8 @@ import dottedCircle from './../../assets/icons/dotted-circle.svg';
|
||||
import circledCheckmark from './../../assets/icons/circled-checkmark.svg';
|
||||
import chevronDown from './../../assets/icons/chevron-down.svg';
|
||||
import chevronLeft from './../../assets/icons/chevron-left.svg';
|
||||
import chevronNext from './../../assets/icons/chevron-next.svg';
|
||||
import chevronPrev from './../../assets/icons/chevron-prev.svg';
|
||||
import chevronRight from './../../assets/icons/chevron-right.svg';
|
||||
import eyeVisible from './../../assets/icons/eye-visible.svg';
|
||||
import eyeHidden from './../../assets/icons/eye-hidden.svg';
|
||||
@ -18,6 +22,7 @@ import exclamation from './../../assets/icons/exclamation.svg';
|
||||
import externalLink from './../../assets/icons/external-link.svg';
|
||||
import groupLayers from './../../assets/icons/group-layers.svg';
|
||||
import info from './../../assets/icons/info.svg';
|
||||
import infoAction from './../../assets/icons/info-action.svg';
|
||||
import infoLink from './../../assets/icons/info-link.svg';
|
||||
import launchArrow from './../../assets/icons/launch-arrow.svg';
|
||||
import launchInfo from './../../assets/icons/launch-info.svg';
|
||||
@ -36,12 +41,19 @@ import settings from './../../assets/icons/settings.svg';
|
||||
import sorting from './../../assets/icons/sorting.svg';
|
||||
import sortingActiveDown from './../../assets/icons/sorting-active-down.svg';
|
||||
import sortingActiveUp from './../../assets/icons/sorting-active-up.svg';
|
||||
import statusAlert from './../../assets/icons/status-alert.svg';
|
||||
import statusLocked from './../../assets/icons/status-locked.svg';
|
||||
import statusTracked from './../../assets/icons/status-tracked.svg';
|
||||
import statusUntracked from './../../assets/icons/status-untracked.svg';
|
||||
import tracked from './../../assets/icons/tracked.svg';
|
||||
import unlink from './../../assets/icons/unlink.svg';
|
||||
import checkboxChecked from './../../assets/icons/checkbox-checked.svg';
|
||||
import checkboxUnchecked from './../../assets/icons/checkbox-unchecked.svg';
|
||||
import iconClose from './../../assets/icons/icon-close.svg';
|
||||
import iconNextInactive from './../../assets/icons/icon-next-inactive.svg';
|
||||
import iconNext from './../../assets/icons/icon-next.svg';
|
||||
import iconPlay from './../../assets/icons/icon-play.svg';
|
||||
import iconPause from './../../assets/icons/icon-pause.svg';
|
||||
import iconPrevInactive from './../../assets/icons/icon-prev-inactive.svg';
|
||||
import iconPrev from './../../assets/icons/icon-prev.svg';
|
||||
import navigationPanelRightHide from './../../assets/icons/navigation-panel-right-hide.svg';
|
||||
@ -107,6 +119,9 @@ import oldStop from './../../assets/icons/old-stop.svg';
|
||||
|
||||
const ICONS = {
|
||||
'arrow-down': arrowDown,
|
||||
'arrow-left': arrowLeft,
|
||||
'arrow-left-small': arrowLeftSmall,
|
||||
'arrow-right-small': arrowRightSmall,
|
||||
calendar: calendar,
|
||||
cancel: cancel,
|
||||
clipboard: clipboard,
|
||||
@ -115,12 +130,18 @@ const ICONS = {
|
||||
'circled-checkmark': circledCheckmark,
|
||||
'chevron-down': chevronDown,
|
||||
'chevron-left': chevronLeft,
|
||||
'chevron-next': chevronNext,
|
||||
'chevron-prev': chevronPrev,
|
||||
'chevron-right': chevronRight,
|
||||
'eye-visible': eyeVisible,
|
||||
'eye-hidden': eyeHidden,
|
||||
'external-link': externalLink,
|
||||
'group-layers': groupLayers,
|
||||
info: info,
|
||||
'icon-close': iconClose,
|
||||
'icon-play': iconPlay,
|
||||
'icon-pause': iconPause,
|
||||
'info-action': infoAction,
|
||||
'info-link': infoLink,
|
||||
'arrow-left': arrowLeft,
|
||||
'launch-arrow': launchArrow,
|
||||
@ -140,6 +161,10 @@ const ICONS = {
|
||||
settings: settings,
|
||||
'sorting-active-down': sortingActiveDown,
|
||||
'sorting-active-up': sortingActiveUp,
|
||||
'status-alert': statusAlert,
|
||||
'status-locked': statusLocked,
|
||||
'status-tracked': statusTracked,
|
||||
'status-untracked': statusUntracked,
|
||||
sorting: sorting,
|
||||
tracked: tracked,
|
||||
unlink: unlink,
|
||||
|
||||
@ -21,7 +21,7 @@ const disabledClasses = {
|
||||
const variantClasses = {
|
||||
text: {
|
||||
default:
|
||||
'text-white hover:bg-primary-light hover:text-black active:opacity-80 focus:bg-primary-light focus:text-black',
|
||||
'text-white hover:bg-primary-light hover:text-black active:opacity-80 focus:!bg-primary-light focus:text-black',
|
||||
primary:
|
||||
'text-primary-main hover:bg-primary-main hover:text-white active:opacity-80 focus:bg-primary-main focus:text-white',
|
||||
secondary:
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Typography from '../Typography';
|
||||
import './InputRange.css';
|
||||
@ -23,6 +23,7 @@ const InputRange: React.FC<{
|
||||
inputClassName?: string;
|
||||
labelClassName?: string;
|
||||
labelVariant?: string;
|
||||
showLabel: boolean;
|
||||
}> = ({
|
||||
value,
|
||||
onChange,
|
||||
@ -34,12 +35,16 @@ const InputRange: React.FC<{
|
||||
inputClassName,
|
||||
labelClassName,
|
||||
labelVariant,
|
||||
showLabel = true,
|
||||
}) => {
|
||||
const [rangeValue, setRangeValue] = useState(value);
|
||||
|
||||
// Allow for the value property to update the range value.
|
||||
useEffect(() => setRangeValue(value), [value]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
e => {
|
||||
const rangeValue = e.target.value;
|
||||
const rangeValue = Number(e.target.value);
|
||||
setRangeValue(rangeValue);
|
||||
onChange(rangeValue);
|
||||
},
|
||||
@ -71,14 +76,16 @@ const InputRange: React.FC<{
|
||||
id="myRange"
|
||||
step={step}
|
||||
/>
|
||||
<Typography
|
||||
variant={labelVariant ?? 'subtitle'}
|
||||
component="p"
|
||||
className={classNames('w-8', labelClassName ?? 'text-white')}
|
||||
>
|
||||
{rangeValue}
|
||||
{unit}
|
||||
</Typography>
|
||||
{showLabel && (
|
||||
<Typography
|
||||
variant={labelVariant ?? 'subtitle'}
|
||||
component="p"
|
||||
className={classNames('w-8', labelClassName ?? 'text-white')}
|
||||
>
|
||||
{rangeValue}
|
||||
{unit}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,99 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import debounce from 'lodash.debounce';
|
||||
import { IconButton, Icon } from '../';
|
||||
|
||||
import './LegacyCinePlayerCustomInputRange.css';
|
||||
|
||||
const LegacyCinePlayer = ({
|
||||
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="flex flex-row items-center justify-center h-10 border rounded-full LegacyCinePlayer border-primary-light">
|
||||
<IconButton
|
||||
variant="text"
|
||||
color="inherit"
|
||||
size="initial"
|
||||
className="ml-4 mr-3 text-primary-active"
|
||||
onClick={onPlayPauseChangeHandler}
|
||||
>
|
||||
<Icon width="15px" height="15px" name={action[isPlaying].icon} />
|
||||
</IconButton>
|
||||
<div className="flex flex-col justify-center h-full pt-2 pl-1 pr-1 mr-3">
|
||||
<input
|
||||
type="range"
|
||||
name="frameRate"
|
||||
min={minFrameRate}
|
||||
max={maxFrameRate}
|
||||
step={stepFrameRate}
|
||||
value={frameRate}
|
||||
onChange={onFrameRateChangeHandler}
|
||||
/>
|
||||
<p className="-mt-2 text-sm text-primary-light">{`${frameRate.toFixed(
|
||||
1
|
||||
)} fps`}</p>
|
||||
</div>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="initial"
|
||||
className="mr-3 border rounded-full text-primary-active border-primary-active"
|
||||
onClick={onClose}
|
||||
>
|
||||
<Icon name="close" width="15px" height="15px" />
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
LegacyCinePlayer.defaultProps = {
|
||||
isPlaying: false,
|
||||
minFrameRate: 1,
|
||||
maxFrameRate: 90,
|
||||
stepFrameRate: 1,
|
||||
frameRate: 24,
|
||||
onPlayPauseChange: noop,
|
||||
onFrameRateChange: noop,
|
||||
onClose: noop,
|
||||
};
|
||||
|
||||
LegacyCinePlayer.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 LegacyCinePlayer;
|
||||
@ -1,9 +1,9 @@
|
||||
/*
|
||||
* This is a custom input style scoped specifically to CinePlayer
|
||||
* This is a custom input style scoped specifically to LegacyCinePlayer
|
||||
* written in plain CSS with color variables from tailwind
|
||||
* to avoid complex compatibility configuration.
|
||||
*/
|
||||
.CinePlayer input[type='range'] {
|
||||
.LegacyCinePlayer input[type='range'] {
|
||||
-webkit-appearance: none;
|
||||
background: transparent;
|
||||
width: 100%;
|
||||
@ -11,11 +11,11 @@
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.CinePlayer input[type='range']:focus {
|
||||
.LegacyCinePlayer input[type='range']:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.CinePlayer input[type='range']::-webkit-slider-runnable-track {
|
||||
.LegacyCinePlayer input[type='range']::-webkit-slider-runnable-track {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
cursor: pointer;
|
||||
@ -26,7 +26,7 @@
|
||||
border: 0px solid #000000;
|
||||
}
|
||||
|
||||
.CinePlayer input[type='range']::-webkit-slider-thumb {
|
||||
.LegacyCinePlayer input[type='range']::-webkit-slider-thumb {
|
||||
box-shadow: 0px 0px 0px #000000;
|
||||
border: 4px solid #000000;
|
||||
height: 18px;
|
||||
@ -38,11 +38,11 @@
|
||||
margin-top: -9px;
|
||||
}
|
||||
|
||||
.CinePlayer input[type='range']:focus::-webkit-slider-runnable-track {
|
||||
.LegacyCinePlayer input[type='range']:focus::-webkit-slider-runnable-track {
|
||||
@apply bg-primary-light;
|
||||
}
|
||||
|
||||
.CinePlayer input[type='range']::-moz-range-track {
|
||||
.LegacyCinePlayer input[type='range']::-moz-range-track {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
cursor: pointer;
|
||||
@ -53,7 +53,7 @@
|
||||
border: 0px solid #000000;
|
||||
}
|
||||
|
||||
.CinePlayer input[type='range']::-moz-range-thumb {
|
||||
.LegacyCinePlayer input[type='range']::-moz-range-thumb {
|
||||
box-shadow: 0px 0px 0px #000000;
|
||||
border: 2px solid #000000;
|
||||
height: 12px;
|
||||
@ -63,7 +63,7 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.CinePlayer input[type='range']::-ms-track {
|
||||
.LegacyCinePlayer input[type='range']::-ms-track {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
cursor: pointer;
|
||||
@ -73,21 +73,21 @@
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.CinePlayer input[type='range']::-ms-fill-lower {
|
||||
.LegacyCinePlayer 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 {
|
||||
.LegacyCinePlayer 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 {
|
||||
.LegacyCinePlayer input[type='range']::-ms-thumb {
|
||||
margin-top: 1px;
|
||||
box-shadow: 0px 0px 0px #000000;
|
||||
border: 4px solid #000000;
|
||||
@ -98,10 +98,10 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.CinePlayer input[type='range']:focus::-ms-fill-lower {
|
||||
.LegacyCinePlayer input[type='range']:focus::-ms-fill-lower {
|
||||
@apply bg-primary-light;
|
||||
}
|
||||
|
||||
.CinePlayer input[type='range']:focus::-ms-fill-upper {
|
||||
.LegacyCinePlayer input[type='range']:focus::-ms-fill-upper {
|
||||
@apply bg-primary-light;
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
import LegacyCinePlayer from '../LegacyCinePlayer';
|
||||
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
|
||||
import { createComponentTemplate } from '../../../storybook/functions/create-component-story';
|
||||
|
||||
export const argTypes = {
|
||||
component: LegacyCinePlayer,
|
||||
title: 'Components/LegacyCinePlayer',
|
||||
};
|
||||
|
||||
<Meta title="Components/LegacyCinePlayer" component={LegacyCinePlayer} />
|
||||
|
||||
export const cineTemplate = createComponentTemplate(LegacyCinePlayer);
|
||||
|
||||
<Heading
|
||||
title="LegacyCinePlayer"
|
||||
componentRelativePath="LegacyCinePlayer/LegacyCinePlayer.tsx"
|
||||
/>
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Props](#props)
|
||||
- [Contribute](#contribute)
|
||||
|
||||
## Overview
|
||||
|
||||
<Canvas>
|
||||
<Story name="Overview">{cineTemplate.bind({})}</Story>
|
||||
</Canvas>
|
||||
|
||||
## Props
|
||||
|
||||
<ArgsTable of={LegacyCinePlayer} />
|
||||
|
||||
## Contribute
|
||||
|
||||
<Footer componentRelativePath="LegacyCinePlayer/__stories__/legacyCinePlayer.stories.mdx" />
|
||||
2
platform/ui/src/components/LegacyCinePlayer/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
import LegacyCinePlayer from './LegacyCinePlayer';
|
||||
export default LegacyCinePlayer;
|
||||
@ -0,0 +1,149 @@
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Icon, Tooltip } from '../';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const classes = {
|
||||
infoHeader: 'text-base text-primary-light',
|
||||
infoText: 'text-base text-white max-w-24 truncate',
|
||||
firstRow: 'flex flex-col',
|
||||
row: 'flex flex-col ml-4',
|
||||
};
|
||||
|
||||
function LegacyPatientInfo({
|
||||
patientName,
|
||||
patientSex,
|
||||
patientAge,
|
||||
MRN,
|
||||
thickness,
|
||||
spacing,
|
||||
scanner,
|
||||
isOpen,
|
||||
showPatientInfoRef,
|
||||
}) {
|
||||
const { t } = useTranslation('PatientInfo');
|
||||
|
||||
while (patientAge.charAt(0) === '0') {
|
||||
patientAge = patientAge.substr(1);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={showPatientInfoRef}>
|
||||
<Tooltip
|
||||
isSticky
|
||||
isDisabled={!isOpen}
|
||||
position="bottom-right"
|
||||
content={
|
||||
isOpen && (
|
||||
<div className="flex py-2">
|
||||
<div className="flex pt-1">
|
||||
<Icon name="info-link" className="w-4 text-primary-main" />
|
||||
</div>
|
||||
<div className="flex flex-col ml-2">
|
||||
<span
|
||||
className="text-base font-bold text-white"
|
||||
title={patientName}
|
||||
>
|
||||
{patientName}
|
||||
</span>
|
||||
<div className="flex pb-4 mt-4 mb-4 border-b border-secondary-main">
|
||||
<div className={classnames(classes.firstRow)}>
|
||||
<span className={classnames(classes.infoHeader)}>
|
||||
{t('Sex')}
|
||||
</span>
|
||||
<span
|
||||
className={classnames(classes.infoText)}
|
||||
title={patientSex}
|
||||
>
|
||||
{patientSex}
|
||||
</span>
|
||||
</div>
|
||||
<div className={classnames(classes.row)}>
|
||||
<span className={classnames(classes.infoHeader)}>
|
||||
{t('Age')}
|
||||
</span>
|
||||
<span
|
||||
className={classnames(classes.infoText)}
|
||||
title={patientAge}
|
||||
>
|
||||
{patientAge}
|
||||
</span>
|
||||
</div>
|
||||
<div className={classnames(classes.row)}>
|
||||
<span className={classnames(classes.infoHeader)}>
|
||||
{t('MRN')}
|
||||
</span>
|
||||
<span className={classnames(classes.infoText)} title={MRN}>
|
||||
{MRN}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<div className={classnames(classes.firstRow)}>
|
||||
<span className={classnames(classes.infoHeader)}>
|
||||
{t('Thickness')}
|
||||
</span>
|
||||
<span
|
||||
className={classnames(classes.infoText)}
|
||||
title={thickness}
|
||||
>
|
||||
{thickness}
|
||||
</span>
|
||||
</div>
|
||||
<div className={classnames(classes.row)}>
|
||||
<span className={classnames(classes.infoHeader)}>
|
||||
{t('Spacing')}
|
||||
</span>
|
||||
<span
|
||||
className={classnames(classes.infoText)}
|
||||
title={spacing}
|
||||
>
|
||||
{spacing}
|
||||
</span>
|
||||
</div>
|
||||
<div className={classnames(classes.row)}>
|
||||
<span className={classnames(classes.infoHeader)}>
|
||||
{t('Scanner')}
|
||||
</span>
|
||||
<span
|
||||
className={classnames(classes.infoText)}
|
||||
title={scanner}
|
||||
>
|
||||
{scanner}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="relative flex justify-end cursor-pointer">
|
||||
<div className="relative">
|
||||
<Icon name="profile" className="w-5 text-white" />
|
||||
<Icon
|
||||
name="info-link"
|
||||
className="absolute w-5 text-white bg-black"
|
||||
style={{ right: -7, bottom: -10 }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
LegacyPatientInfo.propTypes = {
|
||||
patientName: PropTypes.string,
|
||||
patientSex: PropTypes.string,
|
||||
patientAge: PropTypes.string,
|
||||
MRN: PropTypes.string,
|
||||
thickness: PropTypes.string,
|
||||
spacing: PropTypes.string,
|
||||
scanner: PropTypes.string,
|
||||
isOpen: PropTypes.bool,
|
||||
showPatientInfoRef: PropTypes.object,
|
||||
};
|
||||
|
||||
export default LegacyPatientInfo;
|
||||
2
platform/ui/src/components/LegacyPatientInfo/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
import LegacyPatientInfo from './LegacyPatientInfo';
|
||||
export default LegacyPatientInfo;
|
||||
@ -1,14 +1,14 @@
|
||||
import React from 'react';
|
||||
import ViewportActionBar from './ViewportActionBar';
|
||||
import LegacyViewportActionBar from './LegacyViewportActionBar';
|
||||
|
||||
export default {
|
||||
component: ViewportActionBar,
|
||||
title: 'Components/ViewportActionBar',
|
||||
component: LegacyViewportActionBar,
|
||||
title: 'Components/LegacyViewportActionBar',
|
||||
};
|
||||
|
||||
export const Default = () => (
|
||||
<div className="w-1/2 p-4 h-64">
|
||||
<ViewportActionBar
|
||||
<LegacyViewportActionBar
|
||||
onArrowsClick={direction => alert(`Series ${direction}`)}
|
||||
studyData={{
|
||||
label: 'A',
|
||||
@ -0,0 +1,180 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Icon, ButtonGroup, Button, LegacyCinePlayer } from '../';
|
||||
import useOnClickOutside from '../../utils/useOnClickOutside';
|
||||
import LegacyPatientInfo from '../LegacyPatientInfo';
|
||||
import { StringNumber } from '../../types';
|
||||
|
||||
const LegacyViewportActionBar = ({
|
||||
studyData,
|
||||
showNavArrows,
|
||||
showStatus,
|
||||
showCine,
|
||||
cineProps,
|
||||
showPatientInfo: patientInfoVisibility,
|
||||
onArrowsClick,
|
||||
onDoubleClick,
|
||||
getStatusComponent,
|
||||
}) => {
|
||||
const [showPatientInfo, setShowPatientInfo] = useState(patientInfoVisibility);
|
||||
|
||||
const {
|
||||
label,
|
||||
useAltStyling,
|
||||
studyDate,
|
||||
currentSeries,
|
||||
seriesDescription,
|
||||
patientInformation,
|
||||
} = studyData;
|
||||
|
||||
const {
|
||||
patientName,
|
||||
patientSex,
|
||||
patientAge,
|
||||
MRN,
|
||||
thickness,
|
||||
spacing,
|
||||
scanner,
|
||||
} = patientInformation;
|
||||
|
||||
const onPatientInfoClick = () => setShowPatientInfo(!showPatientInfo);
|
||||
const closePatientInfo = () => setShowPatientInfo(false);
|
||||
const showPatientInfoRef = useRef(null);
|
||||
const clickOutsideListener = useOnClickOutside(
|
||||
showPatientInfoRef,
|
||||
closePatientInfo
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (showPatientInfo) {
|
||||
clickOutsideListener.add();
|
||||
} else {
|
||||
clickOutsideListener.remove();
|
||||
}
|
||||
|
||||
return () => clickOutsideListener.remove();
|
||||
}, [clickOutsideListener, showPatientInfo]);
|
||||
|
||||
const borderColor = useAltStyling ? '#365A6A' : '#1D205A';
|
||||
|
||||
let backgroundColor = '#020424';
|
||||
if (useAltStyling) {
|
||||
backgroundColor = '#031923';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-wrap items-center p-2 -mt-2 border-b select-none"
|
||||
style={{
|
||||
borderColor: borderColor,
|
||||
backgroundColor: backgroundColor,
|
||||
}}
|
||||
onDoubleClick={onDoubleClick}
|
||||
onContextMenu={e => e.preventDefault()}
|
||||
>
|
||||
<div className="flex flex-1 grow mt-2 min-w-48">
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2 text-white text-large">{label}</span>
|
||||
{showStatus && getStatusComponent()}
|
||||
</div>
|
||||
<div className="flex flex-col justify-start ml-4">
|
||||
<div className="flex">
|
||||
<span className="text-base text-white">{studyDate}</span>
|
||||
<span className="pl-2 ml-2 text-base border-l border-primary-light text-primary-light">
|
||||
S: {currentSeries}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
{/* TODO:
|
||||
This is tricky. Our "no-wrap" in truncate means this has a hard
|
||||
length. The overflow forces ellipse. If we don't set max width
|
||||
appropriately, this causes the ActionBar to overflow.
|
||||
Can clean up by setting percentage widths + calc on parent
|
||||
containers
|
||||
*/}
|
||||
<p className="text-base truncate max-w-40 text-primary-light">
|
||||
{seriesDescription}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showNavArrows && !showCine && (
|
||||
<div className="mt-2" style={{ pointerEvents: 'all' }}>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
size="initial"
|
||||
className="px-2 py-1 bg-black"
|
||||
border="light"
|
||||
onClick={() => onArrowsClick('left')}
|
||||
>
|
||||
<Icon name="chevron-left" className="w-4 text-white" />
|
||||
</Button>
|
||||
<Button
|
||||
size="initial"
|
||||
border="light"
|
||||
className="px-2 py-1 bg-black"
|
||||
onClick={() => onArrowsClick('right')}
|
||||
>
|
||||
<Icon name="chevron-right" className="w-4 text-white" />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
)}
|
||||
{showCine && !showNavArrows && (
|
||||
<div className="mt-2 mr-auto min-w-48 max-w-48">
|
||||
<LegacyCinePlayer {...cineProps} />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex h-8 mt-2 ml-4 mr-2" onClick={onPatientInfoClick}>
|
||||
<LegacyPatientInfo
|
||||
showPatientInfoRef={showPatientInfoRef}
|
||||
isOpen={showPatientInfo}
|
||||
patientName={patientName}
|
||||
patientSex={patientSex}
|
||||
patientAge={patientAge}
|
||||
MRN={MRN}
|
||||
thickness={thickness}
|
||||
spacing={spacing}
|
||||
scanner={scanner}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
LegacyViewportActionBar.propTypes = {
|
||||
onArrowsClick: PropTypes.func.isRequired,
|
||||
showNavArrows: PropTypes.bool,
|
||||
showCine: PropTypes.bool,
|
||||
cineProps: PropTypes.object,
|
||||
showPatientInfo: PropTypes.bool,
|
||||
studyData: PropTypes.shape({
|
||||
//
|
||||
useAltStyling: PropTypes.bool,
|
||||
//
|
||||
label: PropTypes.string.isRequired,
|
||||
studyDate: PropTypes.string.isRequired,
|
||||
currentSeries: StringNumber.isRequired,
|
||||
seriesDescription: PropTypes.string.isRequired,
|
||||
patientInformation: PropTypes.shape({
|
||||
patientName: PropTypes.string.isRequired,
|
||||
patientSex: PropTypes.string.isRequired,
|
||||
patientAge: PropTypes.string.isRequired,
|
||||
MRN: PropTypes.string.isRequired,
|
||||
thickness: PropTypes.string.isRequired,
|
||||
spacing: PropTypes.string.isRequired,
|
||||
scanner: PropTypes.string.isRequired,
|
||||
}),
|
||||
}).isRequired,
|
||||
getStatusComponent: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
LegacyViewportActionBar.defaultProps = {
|
||||
cineProps: {},
|
||||
showCine: false,
|
||||
showStatus: true,
|
||||
showNavArrows: true,
|
||||
showPatientInfo: false,
|
||||
};
|
||||
|
||||
export default LegacyViewportActionBar;
|
||||
@ -0,0 +1,2 @@
|
||||
import LegacyViewportActionBar from './LegacyViewportActionBar';
|
||||
export default LegacyViewportActionBar;
|
||||
@ -119,16 +119,10 @@ function PatientInfo({
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="relative flex justify-end cursor-pointer">
|
||||
<div className="relative">
|
||||
<Icon name="profile" className="w-5 text-white" />
|
||||
<Icon
|
||||
name="info-link"
|
||||
className="absolute w-5 text-white bg-black"
|
||||
style={{ right: -7, bottom: -10 }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Icon
|
||||
className="cursor-pointer text-white hover:text-primary-light"
|
||||
name="info-action"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -13,6 +13,15 @@ const ActiveSegmentationConfig = ({
|
||||
setFillAlpha,
|
||||
usePercentage,
|
||||
}) => {
|
||||
const [
|
||||
useOutlineOpacityPercentage,
|
||||
setUseOutlineOpacityPercentage,
|
||||
] = useState(usePercentage);
|
||||
|
||||
const [useFillAlphaPercentage, setUseFillAlphaPercentage] = useState(
|
||||
usePercentage
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex justify-between text-[12px] pt-[13px] px-2">
|
||||
<div className="flex flex-col items-start">
|
||||
@ -57,9 +66,12 @@ const ActiveSegmentationConfig = ({
|
||||
minValue={0}
|
||||
maxValue={usePercentage ? 100 : 1}
|
||||
value={
|
||||
usePercentage ? config.outlineOpacity * 100 : config.outlineOpacity
|
||||
useOutlineOpacityPercentage
|
||||
? config.outlineOpacity * 100
|
||||
: config.outlineOpacity
|
||||
}
|
||||
onChange={value => {
|
||||
setUseOutlineOpacityPercentage(false);
|
||||
dispatch({
|
||||
type: 'SET_OUTLINE_OPACITY',
|
||||
payload: {
|
||||
@ -78,8 +90,11 @@ const ActiveSegmentationConfig = ({
|
||||
<InputRange
|
||||
minValue={0}
|
||||
maxValue={usePercentage ? 100 : 1}
|
||||
value={usePercentage ? config.fillAlpha * 100 : config.fillAlpha}
|
||||
value={
|
||||
useFillAlphaPercentage ? config.fillAlpha * 100 : config.fillAlpha
|
||||
}
|
||||
onChange={value => {
|
||||
setUseFillAlphaPercentage(false);
|
||||
dispatch({
|
||||
type: 'SET_FILL_ALPHA',
|
||||
payload: {
|
||||
@ -125,6 +140,11 @@ const InactiveSegmentationConfig = ({
|
||||
setFillAlphaInactive,
|
||||
usePercentage,
|
||||
}) => {
|
||||
const [
|
||||
useFillAlphaInactivePercentage,
|
||||
setUseFillInactivePercentage,
|
||||
] = useState(usePercentage);
|
||||
|
||||
return (
|
||||
<div className="px-2">
|
||||
<CheckBox
|
||||
@ -150,11 +170,12 @@ const InactiveSegmentationConfig = ({
|
||||
minValue={0}
|
||||
maxValue={usePercentage ? 100 : 1}
|
||||
value={
|
||||
usePercentage
|
||||
useFillAlphaInactivePercentage
|
||||
? config.fillAlphaInactive * 100
|
||||
: config.fillAlphaInactive
|
||||
}
|
||||
onChange={value => {
|
||||
setUseFillInactivePercentage(false);
|
||||
dispatch({
|
||||
type: 'SET_FILL_ALPHA_INACTIVE',
|
||||
payload: {
|
||||
|
||||
@ -26,7 +26,7 @@ const classes = {
|
||||
baseClasses.Button,
|
||||
!isExpanded &&
|
||||
!primary.isActive &&
|
||||
'hover:bg-primary-dark hover:border-primary-dark'
|
||||
'hover:!bg-primary-dark hover:border-primary-dark'
|
||||
),
|
||||
Interface: 'h-full flex flex-row items-center',
|
||||
Primary: ({ primary, isExpanded }) =>
|
||||
@ -34,7 +34,7 @@ const classes = {
|
||||
baseClasses.Primary,
|
||||
primary.isActive
|
||||
? isExpanded
|
||||
? 'border-primary-dark !bg-primary-dark hover:border-primary-dark text-primary-light'
|
||||
? 'border-primary-dark !bg-primary-dark hover:border-primary-dark !text-primary-light'
|
||||
: `${
|
||||
primary.isToggle
|
||||
? 'border-secondary-dark bg-secondary-light'
|
||||
@ -45,7 +45,7 @@ const classes = {
|
||||
${
|
||||
isExpanded
|
||||
? 'border-primary-dark bg-primary-dark !text-primary-light'
|
||||
: 'border-secondary-dark bg-secondary-dark group-hover/button:border-primary-dark group-hover/button:text-primary-light hover:bg-primary-dark hover:border-primary-dark focus:!text-black'
|
||||
: 'border-secondary-dark bg-secondary-dark group-hover/button:border-primary-dark group-hover/button:text-primary-light hover:!bg-primary-dark hover:border-primary-dark focus:!text-black'
|
||||
}
|
||||
`
|
||||
),
|
||||
|
||||
@ -24,13 +24,13 @@ const ToolbarButton = ({
|
||||
const classes = {
|
||||
tool: isActive
|
||||
? 'text-black'
|
||||
: 'text-common-bright hover:bg-primary-dark hover:text-primary-light',
|
||||
: 'text-common-bright hover:!bg-primary-dark hover:text-primary-light',
|
||||
toggle: isActive
|
||||
? 'text-[#348CFD]'
|
||||
: 'text-common-bright hover:bg-primary-dark hover:text-primary-light',
|
||||
? '!text-[#348CFD]'
|
||||
: 'text-common-bright hover:!bg-primary-dark hover:text-primary-light',
|
||||
action: isActive
|
||||
? 'text-black'
|
||||
: 'text-common-bright hover:bg-primary-dark hover:text-primary-light',
|
||||
: 'text-common-bright hover:!bg-primary-dark hover:text-primary-light',
|
||||
};
|
||||
|
||||
const bgClasses = {
|
||||
|
||||
@ -23,6 +23,11 @@ const arrowPositionStyle = {
|
||||
right: -15,
|
||||
transform: 'rotate(-270deg)',
|
||||
},
|
||||
top: {
|
||||
bottom: -15,
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%) rotate(180deg)',
|
||||
},
|
||||
};
|
||||
|
||||
const Tooltip = ({
|
||||
@ -69,7 +74,7 @@ const Tooltip = ({
|
||||
>
|
||||
<div
|
||||
className={classnames(
|
||||
'relative tooltip-box bg-primary-dark border border-secondary-main text-white text-base rounded inset-x-auto top-full w-max-content',
|
||||
'relative tooltip-box bg-primary-dark border border-secondary-light text-white text-base rounded inset-x-auto top-full w-max-content',
|
||||
{
|
||||
'py-1 px-4': !tight,
|
||||
}
|
||||
@ -77,12 +82,12 @@ const Tooltip = ({
|
||||
>
|
||||
{typeof content === 'string' ? t(content) : content}
|
||||
<svg
|
||||
className="absolute h-4 text-primary-dark stroke-secondary-main"
|
||||
className="absolute h-4 text-primary-dark stroke-secondary-light"
|
||||
style={arrowPositionStyle[position]}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path fill="currentColor" d="M24 22h-24l12-20z" />
|
||||
<path fill="currentColor" d="M24 22l-12-20l-12 20" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@ -107,6 +112,7 @@ Tooltip.propTypes = {
|
||||
'bottom-right',
|
||||
'left',
|
||||
'right',
|
||||
'top',
|
||||
]),
|
||||
isSticky: PropTypes.bool,
|
||||
tight: PropTypes.bool,
|
||||
|
||||
@ -13,12 +13,29 @@
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.tooltip.tooltip-top .tooltip-box::before {
|
||||
@apply absolute z-10 bg-primary-dark;
|
||||
content: '';
|
||||
width: 14px;
|
||||
height: 1px;
|
||||
bottom: -1px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.tooltip.tooltip-bottom {
|
||||
@apply pt-2 mt-1;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.tooltip.tooltip-top {
|
||||
@apply pb-2 mb-1;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: 100%;
|
||||
}
|
||||
|
||||
.tooltip.tooltip-bottom-left {
|
||||
@apply pt-2 mt-1;
|
||||
left: 0;
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ViewportActionBar, Notification } from '../';
|
||||
import { LegacyViewportActionBar, Notification } from '../';
|
||||
|
||||
const Viewport = ({ viewportIndex, onArrowsClick, studyData, children }) => {
|
||||
return (
|
||||
<div className="relative flex flex-col h-full">
|
||||
<div className="absolute top-0 left-0 w-full">
|
||||
<ViewportActionBar
|
||||
<LegacyViewportActionBar
|
||||
onArrowsClick={onArrowsClick}
|
||||
studyData={studyData}
|
||||
/>
|
||||
|
||||
@ -1,31 +1,31 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Icon, ButtonGroup, Button, CinePlayer } from '../';
|
||||
import React, {
|
||||
MouseEventHandler,
|
||||
ReactElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Icon } from '..';
|
||||
import { useResizeObserver } from '../../hooks';
|
||||
import useOnClickOutside from '../../utils/useOnClickOutside';
|
||||
import PatientInfo from '../PatientInfo';
|
||||
import { StringNumber } from '../../types';
|
||||
|
||||
export type ViewportActionBarProps = {
|
||||
studyData: any;
|
||||
onArrowsClick: (arrow: string) => void;
|
||||
onDoubleClick: MouseEventHandler;
|
||||
getStatusComponent: () => ReactElement;
|
||||
};
|
||||
|
||||
const ViewportActionBar = ({
|
||||
studyData,
|
||||
showNavArrows,
|
||||
showStatus,
|
||||
showCine,
|
||||
cineProps,
|
||||
showPatientInfo: patientInfoVisibility,
|
||||
onArrowsClick,
|
||||
onDoubleClick,
|
||||
getStatusComponent,
|
||||
}) => {
|
||||
const [showPatientInfo, setShowPatientInfo] = useState(patientInfoVisibility);
|
||||
|
||||
const {
|
||||
label,
|
||||
useAltStyling,
|
||||
studyDate,
|
||||
currentSeries,
|
||||
seriesDescription,
|
||||
patientInformation,
|
||||
} = studyData;
|
||||
}: ViewportActionBarProps): JSX.Element => {
|
||||
const { label, studyDate, seriesDescription, patientInformation } = studyData;
|
||||
|
||||
const {
|
||||
patientName,
|
||||
@ -37,11 +37,43 @@ const ViewportActionBar = ({
|
||||
scanner,
|
||||
} = patientInformation;
|
||||
|
||||
// The minimum width that the viewport must be to show the next/prev arrows.
|
||||
const arrowsPresentViewportMinWidth = 300;
|
||||
|
||||
// The space left between the study date and the patient info icon when the series description text is zero width.
|
||||
// With a zero width series description what we have left is:
|
||||
// - a separator (17px)
|
||||
// - patient info icon left padding (4px)
|
||||
// - series description right margin (4px)
|
||||
const zeroWidthSeriesDescriptionSpace = 25;
|
||||
|
||||
const separatorClasses = 'border-l py-2 mx-2 border-secondary-light';
|
||||
const textEllipsisClasses = 'overflow-hidden shrink text-ellipsis';
|
||||
const arrowClasses =
|
||||
'cursor-pointer shrink-0 mr-2 text-white hover:text-primary-light';
|
||||
|
||||
const componentRootElemRef = (elem: HTMLElement) => {
|
||||
setComponentRootElem(elem);
|
||||
};
|
||||
const studyDateElemRef = useRef<HTMLElement>(null);
|
||||
const seriesDescElemRef = useRef<HTMLElement>(null);
|
||||
const showPatientInfoElemRef = useRef<HTMLElement>(null);
|
||||
|
||||
const onPatientInfoClick = () => setShowPatientInfo(!showPatientInfo);
|
||||
const closePatientInfo = () => setShowPatientInfo(false);
|
||||
const showPatientInfoRef = useRef(null);
|
||||
|
||||
const [showPatientInfo, setShowPatientInfo] = useState(false);
|
||||
const [showSeriesDesc, setShowSeriesDesc] = useState(true);
|
||||
const [showArrows, setShowArrows] = useState(true);
|
||||
const [componentRootElem, setComponentRootElem] = useState(null);
|
||||
|
||||
const studyDateClasses = () =>
|
||||
`text-white ${showSeriesDesc ? '' : `mr-1 ${textEllipsisClasses}`}`;
|
||||
|
||||
const patientInfoClasses = () => (showArrows ? '' : 'pl-1 ml-auto');
|
||||
|
||||
const clickOutsideListener = useOnClickOutside(
|
||||
showPatientInfoRef,
|
||||
showPatientInfoElemRef,
|
||||
closePatientInfo
|
||||
);
|
||||
|
||||
@ -55,79 +87,81 @@ const ViewportActionBar = ({
|
||||
return () => clickOutsideListener.remove();
|
||||
}, [clickOutsideListener, showPatientInfo]);
|
||||
|
||||
const borderColor = useAltStyling ? '#365A6A' : '#1D205A';
|
||||
/**
|
||||
* Handles what gets hidden and what gets shown during a resize of the viewport.
|
||||
*/
|
||||
const resizeCallback = useCallback(() => {
|
||||
if (!componentRootElem) {
|
||||
return;
|
||||
}
|
||||
|
||||
let backgroundColor = '#020424';
|
||||
if (useAltStyling) {
|
||||
backgroundColor = '#031923';
|
||||
}
|
||||
const componentRootElemBBox = componentRootElem.getBoundingClientRect();
|
||||
|
||||
// Show or hide the arrows based on the viewport/root element width.
|
||||
if (componentRootElemBBox.width < arrowsPresentViewportMinWidth) {
|
||||
setShowArrows(false);
|
||||
} else {
|
||||
setShowArrows(true);
|
||||
}
|
||||
|
||||
const studyDateElemBBox = studyDateElemRef.current.getBoundingClientRect();
|
||||
const showPatientInfoElemBBox = showPatientInfoElemRef.current.getBoundingClientRect();
|
||||
|
||||
if (
|
||||
showPatientInfoElemBBox.left - studyDateElemBBox.right <=
|
||||
zeroWidthSeriesDescriptionSpace
|
||||
) {
|
||||
// The area to display the series description is zero, so don't show the series description element.
|
||||
setShowSeriesDesc(false);
|
||||
} else {
|
||||
setShowSeriesDesc(true);
|
||||
}
|
||||
}, [componentRootElem]);
|
||||
|
||||
useResizeObserver(componentRootElem, resizeCallback);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-wrap items-center p-2 -mt-2 border-b select-none"
|
||||
style={{
|
||||
borderColor: borderColor,
|
||||
backgroundColor: backgroundColor,
|
||||
}}
|
||||
ref={componentRootElemRef}
|
||||
className="pointer-events-auto select-none text-base flex overflow-visible whitespace-nowrap h-8 items-center px-2 shrink-0"
|
||||
onDoubleClick={onDoubleClick}
|
||||
onContextMenu={e => e.preventDefault()}
|
||||
>
|
||||
<div className="flex flex-1 grow mt-2 min-w-48">
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2 text-white text-large">{label}</span>
|
||||
{showStatus && getStatusComponent()}
|
||||
</div>
|
||||
<div className="flex flex-col justify-start ml-4">
|
||||
<div className="flex">
|
||||
<span className="text-base text-white">{studyDate}</span>
|
||||
<span className="pl-2 ml-2 text-base border-l border-primary-light text-primary-light">
|
||||
S: {currentSeries}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
{/* TODO:
|
||||
This is tricky. Our "no-wrap" in truncate means this has a hard
|
||||
length. The overflow forces ellipse. If we don't set max width
|
||||
appropriately, this causes the ActionBar to overflow.
|
||||
Can clean up by setting percentage widths + calc on parent
|
||||
containers
|
||||
*/}
|
||||
<p className="text-base truncate max-w-40 text-primary-light">
|
||||
{seriesDescription}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showNavArrows && !showCine && (
|
||||
<div className="mt-2" style={{ pointerEvents: 'all' }}>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
size="initial"
|
||||
className="px-2 py-1 bg-black"
|
||||
border="light"
|
||||
onClick={() => onArrowsClick('left')}
|
||||
>
|
||||
<Icon name="chevron-left" className="w-4 text-white" />
|
||||
</Button>
|
||||
<Button
|
||||
size="initial"
|
||||
border="light"
|
||||
className="px-2 py-1 bg-black"
|
||||
onClick={() => onArrowsClick('right')}
|
||||
>
|
||||
<Icon name="chevron-right" className="w-4 text-white" />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
{getStatusComponent()}
|
||||
{!!label?.length && (
|
||||
<span className="ml-1 text-aqua-pale text-large">{label}</span>
|
||||
)}
|
||||
{showCine && !showNavArrows && (
|
||||
<div className="mt-2 mr-auto min-w-48 max-w-48">
|
||||
<CinePlayer {...cineProps} />
|
||||
</div>
|
||||
<div className={separatorClasses}></div>
|
||||
<span ref={studyDateElemRef} className={studyDateClasses()}>
|
||||
{studyDate}
|
||||
</span>
|
||||
{showSeriesDesc && (
|
||||
<>
|
||||
<div className={separatorClasses}></div>
|
||||
<span
|
||||
ref={seriesDescElemRef}
|
||||
className={`mr-1 text-aqua-pale ${textEllipsisClasses}`}
|
||||
>
|
||||
{seriesDescription}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<div className="flex h-8 mt-2 ml-4 mr-2" onClick={onPatientInfoClick}>
|
||||
{showArrows && (
|
||||
<>
|
||||
<Icon
|
||||
className={`ml-auto ${arrowClasses}`}
|
||||
name="chevron-prev"
|
||||
onClick={() => onArrowsClick('left')}
|
||||
/>
|
||||
<Icon
|
||||
className={arrowClasses}
|
||||
name="chevron-next"
|
||||
onClick={() => onArrowsClick('right')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className={patientInfoClasses()} onClick={onPatientInfoClick}>
|
||||
<PatientInfo
|
||||
showPatientInfoRef={showPatientInfoRef}
|
||||
showPatientInfoRef={showPatientInfoElemRef}
|
||||
isOpen={showPatientInfo}
|
||||
patientName={patientName}
|
||||
patientSex={patientSex}
|
||||
@ -144,17 +178,12 @@ const ViewportActionBar = ({
|
||||
|
||||
ViewportActionBar.propTypes = {
|
||||
onArrowsClick: PropTypes.func.isRequired,
|
||||
showNavArrows: PropTypes.bool,
|
||||
showCine: PropTypes.bool,
|
||||
cineProps: PropTypes.object,
|
||||
showPatientInfo: PropTypes.bool,
|
||||
studyData: PropTypes.shape({
|
||||
//
|
||||
useAltStyling: PropTypes.bool,
|
||||
//
|
||||
label: PropTypes.string.isRequired,
|
||||
studyDate: PropTypes.string.isRequired,
|
||||
currentSeries: StringNumber.isRequired,
|
||||
seriesDescription: PropTypes.string.isRequired,
|
||||
patientInformation: PropTypes.shape({
|
||||
patientName: PropTypes.string.isRequired,
|
||||
@ -169,12 +198,4 @@ ViewportActionBar.propTypes = {
|
||||
getStatusComponent: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
ViewportActionBar.defaultProps = {
|
||||
cineProps: {},
|
||||
showCine: false,
|
||||
showStatus: true,
|
||||
showNavArrows: true,
|
||||
showPatientInfo: false,
|
||||
};
|
||||
|
||||
export default ViewportActionBar;
|
||||
|
||||
@ -18,6 +18,8 @@ import InputMultiSelect from './InputMultiSelect';
|
||||
import InputText from './InputText';
|
||||
import Label from './Label';
|
||||
import LayoutSelector from './LayoutSelector';
|
||||
import LegacyCinePlayer from './LegacyCinePlayer';
|
||||
import LegacyViewportActionBar from './LegacyViewportActionBar';
|
||||
import MeasurementTable from './MeasurementTable';
|
||||
import Modal from './Modal';
|
||||
import NavBar from './NavBar';
|
||||
@ -53,7 +55,6 @@ import Tooltip from './Tooltip';
|
||||
import TooltipClipboard from './TooltipClipboard';
|
||||
import Typography from './Typography';
|
||||
import Viewport from './Viewport';
|
||||
import ViewportActionBar from './ViewportActionBar';
|
||||
import ViewportDownloadForm from './ViewportDownloadForm';
|
||||
import ViewportGrid from './ViewportGrid';
|
||||
import ViewportPane from './ViewportPane';
|
||||
@ -68,6 +69,7 @@ import InputRange from './InputRange';
|
||||
import InputNumber from './InputNumber';
|
||||
import CheckBox from './CheckBox';
|
||||
import LoadingIndicatorProgress from './LoadingIndicatorProgress';
|
||||
import ViewportActionBar from './ViewportActionBar';
|
||||
|
||||
export {
|
||||
AboutModal,
|
||||
@ -77,9 +79,9 @@ export {
|
||||
HotkeysPreferences,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
ContextMenu,
|
||||
CinePlayer,
|
||||
CheckBox,
|
||||
CinePlayer,
|
||||
ContextMenu,
|
||||
DateRange,
|
||||
Dialog,
|
||||
Dropdown,
|
||||
@ -100,6 +102,8 @@ export {
|
||||
ImageScrollbar,
|
||||
Label,
|
||||
LayoutSelector,
|
||||
LegacyCinePlayer,
|
||||
LegacyViewportActionBar,
|
||||
LoadingIndicatorProgress,
|
||||
MeasurementTable,
|
||||
Modal,
|
||||
|
||||
3
platform/ui/src/hooks/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import useResizeObserver from './useResizeObserver';
|
||||
|
||||
export { useResizeObserver };
|
||||
28
platform/ui/src/hooks/useResizeObserver.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* A resizeObserver React hook that with useEffect attaches a ResizeObserver to the
|
||||
* given element such that the given callback is invoked whenever the element is resized.
|
||||
* <p>
|
||||
* Care is taken to disconnect the ResizeObserver whenever either the element or the callback change.
|
||||
*
|
||||
* @param elem the element to listen for resizing
|
||||
* @param callback the callback to invoke when the element is resized
|
||||
*/
|
||||
const useResizeObserver = (
|
||||
elem: HTMLElement,
|
||||
callback: ResizeObserverCallback
|
||||
): void => {
|
||||
useEffect(() => {
|
||||
if (!elem || !callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(callback);
|
||||
resizeObserver.observe(elem);
|
||||
|
||||
return () => resizeObserver.disconnect();
|
||||
}, [elem, callback]);
|
||||
};
|
||||
|
||||
export default useResizeObserver;
|
||||
@ -62,6 +62,8 @@ export {
|
||||
InputText,
|
||||
Label,
|
||||
LayoutSelector,
|
||||
LegacyCinePlayer,
|
||||
LegacyViewportActionBar,
|
||||
LoadingIndicatorProgress,
|
||||
MeasurementTable,
|
||||
Modal,
|
||||
|
||||
@ -55,11 +55,17 @@ module.exports = {
|
||||
|
||||
customgreen: {
|
||||
100: '#05D97C',
|
||||
200: '#0FD97C',
|
||||
},
|
||||
|
||||
customblue: {
|
||||
100: '#c4fdff',
|
||||
200: '#38daff',
|
||||
300: '#1D204D',
|
||||
},
|
||||
|
||||
customgray: {
|
||||
100: '#262943',
|
||||
},
|
||||
|
||||
gray: {
|
||||
@ -257,14 +263,15 @@ module.exports = {
|
||||
full: '100%',
|
||||
screen: '100vh',
|
||||
}),
|
||||
inset: {
|
||||
inset: theme => ({
|
||||
...theme('spacing'),
|
||||
'0': '0',
|
||||
auto: 'auto',
|
||||
full: '100%',
|
||||
viewport: '0.5rem',
|
||||
'1/2': '50%',
|
||||
'viewport-scrollbar': '1.3rem',
|
||||
},
|
||||
}),
|
||||
letterSpacing: {
|
||||
tighter: '-0.05em',
|
||||
tight: '-0.025em',
|
||||
|
||||
@ -63,11 +63,17 @@ module.exports = {
|
||||
|
||||
customgreen: {
|
||||
100: '#05D97C',
|
||||
200: '#0FD97C',
|
||||
},
|
||||
|
||||
customblue: {
|
||||
100: '#c4fdff',
|
||||
200: '#38daff',
|
||||
300: '#1D204D',
|
||||
},
|
||||
|
||||
customgray: {
|
||||
100: '#262943',
|
||||
},
|
||||
|
||||
gray: {
|
||||
@ -305,14 +311,15 @@ module.exports = {
|
||||
screen: '100vh',
|
||||
inherit: 'inherit',
|
||||
}),
|
||||
inset: {
|
||||
inset: theme => ({
|
||||
...theme('spacing'),
|
||||
'0': '0',
|
||||
auto: 'auto',
|
||||
full: '100%',
|
||||
viewport: '0.5rem',
|
||||
'1/2': '50%',
|
||||
'viewport-scrollbar': '1.3rem',
|
||||
},
|
||||
}),
|
||||
minHeight: theme => ({
|
||||
...theme('spacing'),
|
||||
'0': '0',
|
||||
|
||||
24
yarn.lock
@ -17919,9 +17919,27 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.26, postcss@^7.0.2
|
||||
picocolors "^0.2.1"
|
||||
source-map "^0.6.1"
|
||||
|
||||
postcss@^8.2.15, postcss@^8.3.11, postcss@^8.3.5, postcss@^8.4.14, postcss@^8.4.17, postcss@^8.4.18, postcss@^8.4.19:
|
||||
postcss@^8.2.15, postcss@^8.3.11, postcss@^8.3.5:
|
||||
version "8.4.14"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf"
|
||||
integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==
|
||||
dependencies:
|
||||
nanoid "^3.3.4"
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
postcss@^8.4.14:
|
||||
version "8.4.16"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.16.tgz#33a1d675fac39941f5f445db0de4db2b6e01d43c"
|
||||
integrity sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==
|
||||
dependencies:
|
||||
nanoid "^3.3.4"
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
postcss@^8.4.17, postcss@^8.4.18, postcss@^8.4.19:
|
||||
version "8.4.21"
|
||||
resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4"
|
||||
integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==
|
||||
dependencies:
|
||||
nanoid "^3.3.4"
|
||||
@ -21030,7 +21048,7 @@ table@^6.0.9:
|
||||
|
||||
tailwindcss@3.2.4:
|
||||
version "3.2.4"
|
||||
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.4.tgz#afe3477e7a19f3ceafb48e4b083e292ce0dc0250"
|
||||
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.2.4.tgz#afe3477e7a19f3ceafb48e4b083e292ce0dc0250"
|
||||
integrity sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==
|
||||
dependencies:
|
||||
arg "^5.0.2"
|
||||
|
||||