feat: Snapshot Download Tool (#840)

* feat(Download ScreenShot Dialog)

- UI part of it

* feat(Download ScreenShot Dialog)

- UI part of it

* feat(Download ScreenShot Dialog)

- Missing Annotations showing controls

* feat(Download ScreenShot Dialog)

- Missing Annotations showing controls

* feat(Download ScreenShot Dialog)

- Partial Pull Request - Not ready to Prod yet.

* feat(Download ScreenShot Dialog)

  - UI - v2

* feat(Download ScreenShot Dialog)

  - UI - v3

* feat(Download ScreenShot Dialog)

  - Resizing tool

* feat(Download ScreenShot Dialog)

  - Resizing tool

* feat(Download ScreenShot Dialog)

  - Removing hardcoded CSS left

* feat(Download ScreenShot Dialog)

  - Bugfixes

* feat(Download ScreenShot Dialog)

  - Bugfixes - Escape closes the modal

* feat(Download ScreenShot Dialog)

  - Bugfixes - Responsive issues

* WIP: Refactoring DownloadDialog

* WIP: Refactoring DownloadDialog

* Align label and hide background canvas

* Add constantes and remove code not used

* Reset state on close modal

* Extract cornerstone to connect

* Fix modal zindex and add custom canvas classname

* CR Update: Add proptypes and fix useeffect warning

* CR Update: Refactor hooks, styles and imports

* CR Update: Remove important from css

* CR Update: Update core utils test of index
This commit is contained in:
Romulo Bordezani authored and Danny Brown committed 2019-10-25 22:46:32 -04:00
1 parent 8cf1e54490
commit 450e0981a5
19 files changed
+824 -96

No files matched your search

@@ -0,0 +1,335 @@
import React, { useEffect, useState, createRef } from 'react';
import Modal from 'react-bootstrap-modal';
import PropTypes from 'prop-types';
import './DownloadDialog.styl';
import { TextInput, Select } from '@ohif/ui';
import { withTranslation } from '../../utils/LanguageProvider';
const FILE_TYPE_OPTIONS = [
{
key: 'jpg',
value: 'jpg',
},
{
key: 'png',
value: 'png',
},
];
const DEFAULT_FILENAME = 'image';
const DownloadDialog = ({
t,
isOpen,
activeViewport,
onClose,
updateViewportPreview,
enableViewport,
disableViewport,
toggleAnnotations,
loadImage,
downloadBlob,
defaultSize,
minimumSize,
canvasClass,
}) => {
const [filename, setFilename] = useState(DEFAULT_FILENAME);
const [fileType, setFileType] = useState('jpg');
const [height, setHeight] = useState(defaultSize);
const [width, setWidth] = useState(defaultSize);
const [showAnnotations, setShowAnnotations] = useState(true);
const [keepAspect, setKeepAspect] = useState(true);
const [lastImage, setLastImage] = useState();
const [viewportElement, setViewportElement] = useState();
const [viewportElementHeight, setViewportElementHeight] = useState(
minimumSize
);
const [viewportElementWidth, setViewportElementWidth] = useState(minimumSize);
const [downloadCanvas, setDownloadCanvas] = useState({
ref: createRef(),
width: minimumSize,
height: minimumSize,
});
const [viewportPreview, setViewportPreview] = useState({
src: null,
width: minimumSize,
height: minimumSize,
});
useEffect(() => {
enableViewport(viewportElement);
return () => {
disableViewport(viewportElement);
setHeight(defaultSize);
setWidth(defaultSize);
};
}, [defaultSize, disableViewport, enableViewport, viewportElement]);
useEffect(() => {
const loadAndUpdateViewports = async () => {
const {
image,
width: scaledWidth,
height: scaledHeight,
} = await loadImage(activeViewport, viewportElement, width, height);
setLastImage(image);
toggleAnnotations(showAnnotations, viewportElement);
setViewportElementHeight(scaledHeight);
setViewportElementWidth(scaledWidth);
setDownloadCanvas(state => ({
...state,
height: scaledHeight,
width: scaledWidth,
}));
const {
dataUrl,
width: viewportElementWidth,
height: viewportElementHeight,
} = await updateViewportPreview(
viewportElement,
downloadCanvas.ref.current,
fileType
);
setViewportPreview(state => ({
...state,
src: dataUrl,
width: viewportElementWidth,
height: viewportElementHeight,
}));
};
loadAndUpdateViewports();
}, [
activeViewport,
viewportElement,
showAnnotations,
height,
width,
loadImage,
toggleAnnotations,
updateViewportPreview,
fileType,
downloadCanvas.ref,
]);
const onHeightChange = () => {
const newHeight = event.target.value;
setHeight(newHeight);
setViewportElementHeight(newHeight);
setDownloadCanvas(state => ({
...state,
height: newHeight,
}));
if (keepAspect) {
const multiplier = newHeight / lastImage.height;
const newWidth = Math.round(lastImage.width * multiplier);
setWidth(newWidth);
setViewportElementWidth(newWidth);
setDownloadCanvas(state => ({
...state,
width: newWidth,
}));
}
};
const onWidthChange = event => {
const newWidth = event.target.value;
setWidth(newWidth);
setViewportElementWidth(newWidth);
setDownloadCanvas(state => ({
...state,
width: newWidth,
}));
if (keepAspect) {
const multiplier = newWidth / lastImage.width;
const newHeight = Math.round(lastImage.height * multiplier);
setHeight(newHeight);
setViewportElementHeight(newHeight);
setDownloadCanvas(state => ({
...state,
height: newHeight,
}));
}
};
const downloadImage = () => {
downloadBlob(
filename || DEFAULT_FILENAME,
fileType,
viewportElement,
downloadCanvas.ref.current
);
};
return (
<Modal
show={isOpen}
onHide={onClose}
aria-labelledby="ModalHeader"
className="DownloadDialog modal fade themed in"
backdrop={false}
large={true}
keyboard={true}
>
<Modal.Header closeButton>
<Modal.Title>{t('Download High Quality Image')}</Modal.Title>
</Modal.Header>
<Modal.Body>
<div className="title">
{t(
'Please specify the dimensions, filename, and desired type for the output image.'
)}
</div>
<div className="file-info-container">
<div className="col">
<div className="width">
<TextInput
type="number"
min={minimumSize}
value={width}
label={t('Image width (px)')}
onChange={onWidthChange}
/>
</div>
<div className="height">
<TextInput
type="number"
min={minimumSize}
value={height}
label={t('Image height (px)')}
onChange={onHeightChange}
/>
</div>
</div>
<div className="col">
<div className="file-name">
<TextInput
type="text"
value={filename}
onChange={event => setFilename(event.target.value)}
label={t('File name')}
id="file-name"
/>
</div>
<div className="file-type">
<Select
value={fileType}
onChange={event => setFileType(event.target.value)}
options={FILE_TYPE_OPTIONS}
label={t('File type')}
/>
</div>
</div>
<div className="col">
<div className="show-annotations">
<label htmlFor="show-annotations" className="form-check-label">
<input
id="show-annotations"
type="checkbox"
className="form-check-input"
checked={showAnnotations}
onChange={event => setShowAnnotations(event.target.checked)}
/>
{t('Show Annotations')}
</label>
</div>
</div>
</div>
<div
style={{
height: viewportElementHeight,
width: viewportElementWidth,
position: 'absolute',
left: '9999px',
}}
ref={ref => setViewportElement(ref)}
>
<canvas
className={canvasClass}
style={{
height: downloadCanvas.height,
width: downloadCanvas.width,
display: 'block',
}}
width={downloadCanvas.width}
height={downloadCanvas.height}
ref={downloadCanvas.ref}
></canvas>
</div>
<div className="preview">
<h4> {t('Image Preview')}</h4>
<img
className="viewport-preview"
src={viewportPreview.src}
alt="Viewport Preview"
style={{
height: viewportPreview.height,
width: viewportPreview.width,
}}
/>
</div>
<div className="actions">
<div className="action-cancel">
<button type="button" className="btn btn-danger" onClick={onClose}>
{t('Cancel')}
</button>
</div>
<div className="action-save">
<button onClick={downloadImage} className="btn btn-primary">
{t('Download')}
</button>
</div>
</div>
</Modal.Body>
</Modal>
);
};
DownloadDialog.propTypes = {
t: PropTypes.func.isRequired,
isOpen: PropTypes.bool.isRequired,
activeViewport: PropTypes.object,
onClose: PropTypes.func.isRequired,
updateViewportPreview: PropTypes.func.isRequired,
enableViewport: PropTypes.func.isRequired,
disableViewport: PropTypes.func.isRequired,
toggleAnnotations: PropTypes.func.isRequired,
loadImage: PropTypes.func.isRequired,
downloadBlob: PropTypes.func.isRequired,
defaultSize: PropTypes.number.isRequired,
minimumSize: PropTypes.number.isRequired,
canvasClass: PropTypes.string.isRequired,
};
export default withTranslation('DownloadDialog')(DownloadDialog);
@@ -0,0 +1,107 @@
@import './../../design/styles/common/global.styl'
@import './../../design/styles/common/form.styl'
@import './../../design/styles/common/button.styl'
.DownloadDialog
color: var(--text-secondary-color);
filter: drop-shadow(0 0 3px var(--ui-gray-darkest));
border: none;
border-radius: 8px;
width: inherit;
padding: 15px;
background: transparent;
z-index: 1080 !important;
.title
margin: 0;
font-weight: bold;
.file-info-container
display: flex;
flex-direction: row;
margin: 20px 0;
border-radius: 5px;
padding: 20px 10px 0;
background-color: #16202b;
.form-control.input-ohif
padding: 6px 12px;
@media screen and (max-width: 1023px)
flex-direction: column;
.col
flex-grow: 1;
.input-ohif
margin-left: 15px;
@media screen and (max-width: 1023px)
margin-left: 0;
margin-top: 5px;
width: 100%;
.file-type
.select-ohif
margin-left: 17px;
@media screen and (max-width: 1023px)
margin-left: 0;
width: 100%;
.show-annotations
font-weight: bold;
line-height: 30px;
input
margin-right: 7px;
vertical-align: middle;
label
display: flex;
justify-content: center;
align-items: center;
.preview
display: flex;
flex-direction column;
height: fit-content;
background-color: #16202b;
width: fit-content;
padding: 10px;
border-radius: 5px;
align-self: center;
@media screen and (max-width: 1023px)
width: 100%;
justify-content: center;
align-items: center;
.viewport-preview
max-height: 512px;
max-width: 512px;
h4
width: 100%;
text-align center;
font-size: 1.3em;
margin: 0 0 10px;
.preview-container
width: auto;
height: 100%;
max-height: 400px;
object-fit contain;
.actions
display: flex;
height: 60px;
flex-wrap: nowrap;
justify-content: flex-end;
align-items: center;
.action-cancel
margin: 0 20px;
.actions-save
margin: 0 0 0 10px;
.modal-dialog
height: 100%;
.modal-body
display: flex;
flex-direction: column;
@@ -0,0 +1,39 @@
---
name: Download Dialog
menu: Components
route: /components/download-dialog
---
import { Playground, Props } from 'docz'
import { State } from 'react-powerplug'
import { DownloadDialog } from './../index'
import NameSpace from '../../../__docs__/NameSpace'
# Download Dialog
## Basic usage
<Playground>
<State
initial={{}}
>
{({ state, setState }) => (
<React.Fragment>
<div>
<pre>{JSON.stringify(state, null, 2)}</pre>
</div>
<div style={{ maxWidth: '400px', margin: '0 auto' }}>
<DownloadDialog />
</div>
</React.Fragment>
)}
</State>
</Playground>
## API
<Props of={DownloadDialog} />
## Translation Namespace
<NameSpace name="DownloadDialog" />
@@ -0,0 +1,2 @@
import DownloadDialog from './DownloadDialog';
export { DownloadDialog };
+2
View File
@@ -11,6 +11,7 @@ import {
import { Checkbox } from './checkbox';
import { CineDialog } from './cineDialog';
import { DownloadDialog } from './downloadDialog';
import { QuickSwitch } from './quickSwitch';
import { RoundedButtonGroup } from './roundedButtonGroup';
import { SelectTree } from './selectTree';
@@ -22,6 +23,7 @@ import { Tooltip } from './tooltip';
export {
Checkbox,
CineDialog,
DownloadDialog,
LayoutButton,
LayoutChooser,
MeasurementTable,