PLT-616: Enable playing of animated GIF in thumbnails and preview
Этот коммит содержится в:
10
api/file.go
10
api/file.go
@@ -23,6 +23,7 @@ import (
|
|||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@@ -331,9 +332,18 @@ func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
w.Header().Set("Cache-Control", "max-age=2592000, public")
|
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 := make(map[string]string)
|
||||||
result["filename"] = filename
|
result["filename"] = filename
|
||||||
result["size"] = size
|
result["size"] = size
|
||||||
|
result["mime"] = mimeType
|
||||||
w.Write([]byte(model.MapToJson(result)))
|
w.Write([]byte(model.MapToJson(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ export default class FileAttachment extends React.Component {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.loadFiles = this.loadFiles.bind(this);
|
this.loadFiles = this.loadFiles.bind(this);
|
||||||
|
this.playGif = this.playGif.bind(this);
|
||||||
|
|
||||||
this.canSetState = false;
|
this.canSetState = false;
|
||||||
this.state = {fileSize: -1};
|
this.state = {fileSize: -1, mime: '', playing: false, loading: false};
|
||||||
}
|
}
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.loadFiles();
|
this.loadFiles();
|
||||||
@@ -93,6 +94,16 @@ export default class FileAttachment extends React.Component {
|
|||||||
|
|
||||||
return true;
|
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() {
|
render() {
|
||||||
var filename = this.props.filename;
|
var filename = this.props.filename;
|
||||||
|
|
||||||
@@ -100,6 +111,38 @@ export default class FileAttachment extends React.Component {
|
|||||||
var fileUrl = utils.getFileUrl(filename);
|
var fileUrl = utils.getFileUrl(filename);
|
||||||
var type = utils.getFileType(fileInfo.ext);
|
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;
|
var thumbnail;
|
||||||
if (type === 'image') {
|
if (type === 'image') {
|
||||||
thumbnail = (
|
thumbnail = (
|
||||||
@@ -107,7 +150,11 @@ export default class FileAttachment extends React.Component {
|
|||||||
ref={filename}
|
ref={filename}
|
||||||
className='post__load'
|
className='post__load'
|
||||||
style={{backgroundImage: 'url(/static/images/load.gif)'}}
|
style={{backgroundImage: 'url(/static/images/load.gif)'}}
|
||||||
/>
|
>
|
||||||
|
{loadingIndicator}
|
||||||
|
{playButton}
|
||||||
|
{loadedFile}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
thumbnail = <div className={'file-icon ' + utils.getIconClassName(type)}/>;
|
thumbnail = <div className={'file-icon ' + utils.getIconClassName(type)}/>;
|
||||||
@@ -119,7 +166,7 @@ export default class FileAttachment extends React.Component {
|
|||||||
filename,
|
filename,
|
||||||
function success(data) {
|
function success(data) {
|
||||||
if (this.canSetState) {
|
if (this.canSetState) {
|
||||||
this.setState({fileSize: parseInt(data.size, 10)});
|
this.setState({fileSize: parseInt(data.size, 10), mime: data.mime});
|
||||||
}
|
}
|
||||||
}.bind(this),
|
}.bind(this),
|
||||||
function error() {}
|
function error() {}
|
||||||
|
|||||||
@@ -38,7 +38,10 @@ export default class ViewImageModal extends React.Component {
|
|||||||
progress: progress,
|
progress: progress,
|
||||||
images: {},
|
images: {},
|
||||||
fileSizes: {},
|
fileSizes: {},
|
||||||
showFooter: false
|
fileMimes: {},
|
||||||
|
showFooter: false,
|
||||||
|
isPlaying: {},
|
||||||
|
isLoading: {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
handleNext(e) {
|
handleNext(e) {
|
||||||
@@ -122,6 +125,28 @@ export default class ViewImageModal extends React.Component {
|
|||||||
this.setState({loaded});
|
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() {
|
componentDidMount() {
|
||||||
$(window).on('keyup', this.handleKeyPress);
|
$(window).on('keyup', this.handleKeyPress);
|
||||||
|
|
||||||
@@ -154,6 +179,10 @@ export default class ViewImageModal extends React.Component {
|
|||||||
var fileType = Utils.getFileType(fileInfo.ext);
|
var fileType = Utils.getFileType(fileInfo.ext);
|
||||||
|
|
||||||
if (fileType === 'image') {
|
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
|
// This is a temporary patch to fix issue with old files using absolute paths
|
||||||
if (fileInfo.path.indexOf('/api/v1/files/get') !== -1) {
|
if (fileInfo.path.indexOf('/api/v1/files/get') !== -1) {
|
||||||
fileInfo.path = fileInfo.path.split('/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);
|
var fileType = Utils.getFileType(fileInfo.ext);
|
||||||
|
|
||||||
if (fileType === 'image') {
|
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
|
// image files just show a preview of the file
|
||||||
content = (
|
content = (
|
||||||
<a
|
<a
|
||||||
href={fileUrl}
|
href={fileUrl}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
>
|
>
|
||||||
|
{loadingIndicator}
|
||||||
|
{playButton}
|
||||||
<img
|
<img
|
||||||
style={{maxHeight: this.state.imgHeight}}
|
style={{maxHeight: this.state.imgHeight}}
|
||||||
ref='image'
|
ref='image'
|
||||||
|
|||||||
@@ -544,6 +544,7 @@ export function applyTheme(theme) {
|
|||||||
if (theme.buttonBg) {
|
if (theme.buttonBg) {
|
||||||
changeCss('.btn.btn-primary', 'background:' + theme.buttonBg, 1);
|
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('.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) {
|
if (theme.buttonColor) {
|
||||||
|
|||||||
@@ -133,12 +133,25 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #FFF;
|
background-color: #FFF;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
&.small {
|
&.small {
|
||||||
background-position: center;
|
background-position: center;
|
||||||
}
|
}
|
||||||
&.normal {
|
&.normal {
|
||||||
background-position: top left;
|
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 {
|
.post-image__thumbnail {
|
||||||
float: left;
|
float: left;
|
||||||
@@ -215,3 +228,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-play-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
font-size: 22px;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|||||||
@@ -228,11 +228,20 @@
|
|||||||
background: #FFF;
|
background: #FFF;
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
img {
|
img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
|
.spinner.file__loading {
|
||||||
|
z-index: 2;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -16px;
|
||||||
|
top: 50%;
|
||||||
|
margin-top: -16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.modal-content{
|
.modal-content{
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user