diff options
-rw-r--r-- | api/file.go | 10 | ||||
-rw-r--r-- | web/react/components/file_attachment.jsx | 53 | ||||
-rw-r--r-- | web/react/components/view_image.jsx | 70 | ||||
-rw-r--r-- | web/react/utils/utils.jsx | 1 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_files.scss | 22 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_modal.scss | 9 |
6 files changed, 161 insertions, 4 deletions
diff --git a/api/file.go b/api/file.go index 142ef7ac7..94eea516a 100644 --- a/api/file.go +++ b/api/file.go @@ -23,6 +23,7 @@ import ( "image/jpeg" "io" "io/ioutil" + "mime" "net/http" "net/url" "os" @@ -331,9 +332,18 @@ func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "max-age=2592000, public") + var mimeType string + ext := filepath.Ext(filename) + if model.IsFileExtImage(ext) { + mimeType = model.GetImageMimeType(ext) + } else { + mimeType = mime.TypeByExtension(ext) + } + result := make(map[string]string) result["filename"] = filename result["size"] = size + result["mime"] = mimeType w.Write([]byte(model.MapToJson(result))) } diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx index c6dff6550..817834334 100644 --- a/web/react/components/file_attachment.jsx +++ b/web/react/components/file_attachment.jsx @@ -10,9 +10,10 @@ export default class FileAttachment extends React.Component { super(props); this.loadFiles = this.loadFiles.bind(this); + this.playGif = this.playGif.bind(this); this.canSetState = false; - this.state = {fileSize: -1}; + this.state = {fileSize: -1, mime: '', playing: false, loading: false}; } componentDidMount() { this.loadFiles(); @@ -93,6 +94,16 @@ export default class FileAttachment extends React.Component { return true; } + playGif(e, fileUrl) { + var img = new Image(); + + this.setState({loading: true}); + img.load(fileUrl); + img.onload = () => this.setState({playing: true, loading: false}); + img.onError = () => this.setState({loading: false}); + + e.stopPropagation(); + } render() { var filename = this.props.filename; @@ -100,6 +111,38 @@ export default class FileAttachment extends React.Component { var fileUrl = utils.getFileUrl(filename); var type = utils.getFileType(fileInfo.ext); + var playButton = ''; + var loadedFile = ''; + var loadingIndicator = ''; + if (this.state.mime === 'image/gif') { + playButton = ( + <div + className='file-play-button' + onClick={(e) => this.playGif(e, fileUrl)} + > + {"►"} + </div> + ); + } + if (this.state.playing) { + loadedFile = ( + <img + className='file__loaded' + src={fileUrl} + /> + ); + playButton = ''; + } + if (this.state.loading) { + loadingIndicator = ( + <img + className='spinner file__loading' + src='/static/images/load.gif' + /> + ); + playButton = ''; + } + var thumbnail; if (type === 'image') { thumbnail = ( @@ -107,7 +150,11 @@ export default class FileAttachment extends React.Component { ref={filename} className='post__load' style={{backgroundImage: 'url(/static/images/load.gif)'}} - /> + > + {loadingIndicator} + {playButton} + {loadedFile} + </div> ); } else { thumbnail = <div className={'file-icon ' + utils.getIconClassName(type)}/>; @@ -119,7 +166,7 @@ export default class FileAttachment extends React.Component { filename, function success(data) { if (this.canSetState) { - this.setState({fileSize: parseInt(data.size, 10)}); + this.setState({fileSize: parseInt(data.size, 10), mime: data.mime}); } }.bind(this), function error() {} diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx index 322e68c17..c8356b2fa 100644 --- a/web/react/components/view_image.jsx +++ b/web/react/components/view_image.jsx @@ -38,7 +38,10 @@ export default class ViewImageModal extends React.Component { progress: progress, images: {}, fileSizes: {}, - showFooter: false + fileMimes: {}, + showFooter: false, + isPlaying: {}, + isLoading: {} }; } handleNext(e) { @@ -122,6 +125,28 @@ export default class ViewImageModal extends React.Component { this.setState({loaded}); } } + playGif(e, filename, fileUrl) { + var isLoading = this.state.isLoading; + var isPlaying = this.state.isPlaying; + + isLoading[filename] = fileUrl; + this.setState({isLoading}); + + var img = new Image(); + img.load(fileUrl); + img.onload = () => { + delete isLoading[filename]; + isPlaying[filename] = fileUrl; + this.setState({isPlaying, isLoading}); + }; + img.onError = () => { + delete isLoading[filename]; + this.setState({isLoading}); + }; + + e.stopPropagation(); + e.preventDefault(); + } componentDidMount() { $(window).on('keyup', this.handleKeyPress); @@ -154,6 +179,10 @@ export default class ViewImageModal extends React.Component { var fileType = Utils.getFileType(fileInfo.ext); if (fileType === 'image') { + if (typeof this.state.isPlaying[filename] !== 'undefined') { + return this.state.isPlaying[filename]; + } + // This is a temporary patch to fix issue with old files using absolute paths if (fileInfo.path.indexOf('/api/v1/files/get') !== -1) { fileInfo.path = fileInfo.path.split('/api/v1/files/get')[1]; @@ -189,12 +218,51 @@ export default class ViewImageModal extends React.Component { var fileType = Utils.getFileType(fileInfo.ext); if (fileType === 'image') { + if (!(filename in this.state.fileMimes)) { + Client.getFileInfo( + filename, + (data) => { + if (this.canSetState) { + var fileMimes = this.state.fileMimes; + fileMimes[filename] = data.mime; + this.setState(fileMimes); + } + }, + () => {} + ); + } + + var playButton = ''; + if (this.state.fileMimes[filename] === 'image/gif' && !(filename in this.state.isLoading) && !(filename in this.state.isPlaying)) { + playButton = ( + <div + className='file-play-button' + onClick={(e) => this.playGif(e, filename, fileUrl)} + > + {"►"} + </div> + ); + } + + var loadingIndicator = ''; + if (this.state.isLoading[filename] === fileUrl) { + loadingIndicator = ( + <img + className='spinner file__loading' + src='/static/images/load.gif' + /> + ); + playButton = ''; + } + // image files just show a preview of the file content = ( <a href={fileUrl} target='_blank' > + {loadingIndicator} + {playButton} <img style={{maxHeight: this.state.imgHeight}} ref='image' diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 5f266bba3..69c026271 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -544,6 +544,7 @@ export function applyTheme(theme) { if (theme.buttonBg) { changeCss('.btn.btn-primary', 'background:' + theme.buttonBg, 1); changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background:' + changeColor(theme.buttonBg, -0.25), 1); + changeCss('.file-play-button', 'color:' + changeColor(theme.buttonBg, -0.25), 1); } if (theme.buttonColor) { diff --git a/web/sass-files/sass/partials/_files.scss b/web/sass-files/sass/partials/_files.scss index 01057423d..1d7754445 100644 --- a/web/sass-files/sass/partials/_files.scss +++ b/web/sass-files/sass/partials/_files.scss @@ -133,12 +133,25 @@ height: 100%; background-color: #FFF; background-repeat: no-repeat; + overflow: hidden; + position: relative; &.small { background-position: center; } &.normal { background-position: top left; } + .spinner.file__loading { + position: absolute; + left: 50%; + margin-left: -16px; + top: 50%; + margin-top: -16px; + } + .file__loaded { + height: 100px; + max-width: initial; + } } .post-image__thumbnail { float: left; @@ -215,3 +228,12 @@ } } } + +.file-play-button { + position: absolute; + right: 0; + bottom: 0; + font-size: 22px; + cursor: pointer; + z-index: 2; +} diff --git a/web/sass-files/sass/partials/_modal.scss b/web/sass-files/sass/partials/_modal.scss index 5570b5ce4..5a8c1899a 100644 --- a/web/sass-files/sass/partials/_modal.scss +++ b/web/sass-files/sass/partials/_modal.scss @@ -228,11 +228,20 @@ background: #FFF; display: table-cell; vertical-align: middle; + position: relative; } img { max-width: 100%; max-height: 100%; } + .spinner.file__loading { + z-index: 2; + position: absolute; + left: 50%; + margin-left: -16px; + top: 50%; + margin-top: -16px; + } } .modal-content{ box-shadow: none; |