diff --git a/frontend/src/assets/icons/rotate.svg b/frontend/src/assets/icons/rotate.svg new file mode 100644 index 0000000000..574ca7b163 --- /dev/null +++ b/frontend/src/assets/icons/rotate.svg @@ -0,0 +1,14 @@ + + + icon-rotate + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/components/file-content-view/image.js b/frontend/src/components/file-content-view/image.js index e764066cac..e6fd7a8dee 100644 --- a/frontend/src/components/file-content-view/image.js +++ b/frontend/src/components/file-content-view/image.js @@ -13,7 +13,8 @@ const { xmindImageSrc // for xmind file } = window.app.pageOptions; -let previousImageUrl; let nextImageUrl; +let previousImageUrl; +let nextImageUrl; if (previousImage) { previousImageUrl = `${siteRoot}lib/${repoID}/file${Utils.encodePath(previousImage)}`; } @@ -55,7 +56,7 @@ class FileContent extends React.Component { // request thumbnails for some files // only for 'file view'. not for 'history/trash file view' let thumbnailURL = ''; - const fileExtList = ['tif', 'tiff', 'psd']; + const fileExtList = ['tif', 'tiff', 'psd', 'heic']; if (!repoEncrypted && fileExtList.includes(fileExt)) { thumbnailURL = `${siteRoot}thumbnail/${repoID}/${thumbnailSizeForOriginal}${Utils.encodePath(filePath)}`; } @@ -63,6 +64,17 @@ class FileContent extends React.Component { // for xmind file const xmindSrc = xmindImageSrc ? `${siteRoot}${xmindImageSrc}` : ''; + const { scale, angle } = this.props; + let style = {}; + if (scale && angle != undefined) { + style = { transform: `scale(${scale}) rotate(${angle}deg)` }; + } else if (scale) { + style = { transform: `scale(${scale})` }; + } else if (angle != undefined) { + style = { transform: `rotate(${angle}deg)` }; + } + + return (
{previousImage && ( @@ -71,14 +83,16 @@ class FileContent extends React.Component { {nextImage && ( )} - {fileName} + {fileName}
); } } FileContent.propTypes = { - tip: PropTypes.string.isRequired, + tip: PropTypes.object.isRequired, + scale: PropTypes.number, + angle: PropTypes.number }; export default FileContent; diff --git a/frontend/src/components/file-view/file-toolbar.js b/frontend/src/components/file-view/file-toolbar.js index 775ca16d15..97e9831354 100644 --- a/frontend/src/components/file-view/file-toolbar.js +++ b/frontend/src/components/file-view/file-toolbar.js @@ -9,6 +9,7 @@ import ShareDialog from '../dialog/share-dialog'; import { seafileAPI } from '../../utils/seafile-api'; import toaster from '../toast'; import Icon from '../../components/icon'; +import ImageZoomer from './image-zoomer'; const propTypes = { isLocked: PropTypes.bool.isRequired, @@ -17,13 +18,15 @@ const propTypes = { isSaving: PropTypes.bool, needSave: PropTypes.bool, toggleLockFile: PropTypes.func.isRequired, - toggleDetailsPanel: PropTypes.func.isRequired + toggleDetailsPanel: PropTypes.func.isRequired, + setImageScale: PropTypes.func, + rotateImage: PropTypes.func }; const { canLockUnlockFile, repoID, repoName, repoEncrypted, parentDir, filePerm, filePath, - fileType, + fileType, fileExt, fileName, canEditFile, err, // fileEnc, // for 'edit', not undefined only for some kinds of files (e.g. text file) @@ -119,6 +122,19 @@ class FileToolbar extends React.Component { return (
+ {(fileType == 'Image' && !err) && ( + <> + + {['psd', 'heic'].indexOf(fileExt) == -1 && ( + + )} + + )} {fileType == 'PDF' && ( }
diff --git a/frontend/src/components/file-view/image-zoomer.js b/frontend/src/components/file-view/image-zoomer.js new file mode 100644 index 0000000000..bbf43e4819 --- /dev/null +++ b/frontend/src/components/file-view/image-zoomer.js @@ -0,0 +1,63 @@ +import React, { useState, useCallback } from 'react'; +import PropTypes from 'prop-types'; +import { Button, Input } from 'reactstrap'; +import Icon from '../../components/icon'; + +import '../../metadata/components/data-process-setter/gallery-slider-setter/index.css'; + +const SCALE_OPTIONS = [0.25, 0.5, 1, 1.5, 2]; +const SCALE_MIN = SCALE_OPTIONS[0]; +const SCALE_MAX = SCALE_OPTIONS[SCALE_OPTIONS.length - 1]; + +const ImageZoomer = ({ setImageScale }) => { + + const [curScale, setScale] = useState(1); + + const scaleImage = useCallback((scale) => { + setImageScale(scale); + }, [setImageScale]); + + const changeScale = useCallback((e) => { + const scale = Number(e.target.value); + setScale(scale); + scaleImage(scale); + }, [scaleImage]); + + const zoomIn = useCallback(() => { + const scale = SCALE_OPTIONS[SCALE_OPTIONS.indexOf(curScale) + 1]; + setScale(scale); + scaleImage(scale); + }, [curScale, scaleImage]); + + const zoomOut = useCallback(() => { + const scale = SCALE_OPTIONS[SCALE_OPTIONS.indexOf(curScale) - 1]; + setScale(scale); + scaleImage(scale); + }, [curScale, scaleImage]); + + return ( +
+ + + +
+ ); +}; + +ImageZoomer.propTypes = { + setImageScale: PropTypes.func +}; + +export default ImageZoomer; diff --git a/frontend/src/css/image-file-view.css b/frontend/src/css/image-file-view.css index 66fdd2dfe7..182401383f 100644 --- a/frontend/src/css/image-file-view.css +++ b/frontend/src/css/image-file-view.css @@ -35,6 +35,7 @@ background: #fff; border-radius: 100%; line-height: 50px; + z-index: 1; } #img-prev { @@ -49,3 +50,7 @@ #img-next:hover { color: #212529; } + +.image-zoomer { + height: 24px; +} diff --git a/frontend/src/file-view.js b/frontend/src/file-view.js index ea06082fee..0780e05fa3 100644 --- a/frontend/src/file-view.js +++ b/frontend/src/file-view.js @@ -7,12 +7,44 @@ import SVG from './components/file-content-view/svg'; import PDF from './components/file-content-view/pdf'; import Video from './components/file-content-view/video'; import Audio from './components/file-content-view/audio'; +import { Utils } from './utils/utils'; +import { gettext } from './utils/constants'; +import ImageAPI from './utils/image-api'; +import toaster from './components/toast'; const { + repoID, filePath, fileType, err } = window.app.pageOptions; class InnerFileView extends React.Component { + constructor() { + super(); + this.state = { + imageScale: 1, + imageAngle: 0 + }; + } + + setImageScale = (scale) => { + this.setState({ + imageScale: scale + }); + }; + + rotateImage = () => { + this.setState({ + imageAngle: (this.state.imageAngle - 90) % 360 // counter-clockwise + }, () => { + // const angleClockwise = this.state.imageAngle + 360; // keep this line for the moment + const angleClockwise = 270; // the API only accept clockwise angles + ImageAPI.rotateImage(repoID, filePath, 360 - angleClockwise).then((res) => { + toaster.success(gettext('Image saved'), { 'id': 'image-saved-tip' }); + }).catch(error => { + toaster.danger(Utils.getErrorMsg(error)); + }); + }); + }; render() { if (err) { @@ -21,10 +53,11 @@ class InnerFileView extends React.Component { ); } + const { imageScale, imageAngle } = this.state; let content; switch (fileType) { case 'Image': - content = } />; + content = } scale={imageScale} angle={imageAngle} />; break; case 'XMind': content = } />; @@ -46,7 +79,11 @@ class InnerFileView extends React.Component { } return ( - + ); } }