1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-06 17:33:18 +00:00

[shared file view] rewrote with react (#3191)

* rewrote 'file view' with react for audio, document, spreadsheet, svg,
unknown files
* code improvement for 'video' file
* bugfix
This commit is contained in:
llj
2019-03-29 15:30:44 +08:00
committed by Daniel Pan
parent a7a9ede448
commit 6c49334ca3
13 changed files with 429 additions and 147 deletions

View File

@@ -109,6 +109,31 @@ module.exports = {
require.resolve('react-dev-utils/webpackHotDevClient'), require.resolve('react-dev-utils/webpackHotDevClient'),
paths.appSrc + "/shared-file-view-pdf.js", paths.appSrc + "/shared-file-view-pdf.js",
], ],
sharedFileViewSVG: [
require.resolve('./polyfills'),
require.resolve('react-dev-utils/webpackHotDevClient'),
paths.appSrc + "/shared-file-view-svg.js",
],
sharedFileViewAudio: [
require.resolve('./polyfills'),
require.resolve('react-dev-utils/webpackHotDevClient'),
paths.appSrc + "/shared-file-view-audio.js",
],
sharedFileViewDocument: [
require.resolve('./polyfills'),
require.resolve('react-dev-utils/webpackHotDevClient'),
paths.appSrc + "/shared-file-view-document.js",
],
sharedFileViewSpreadsheet: [
require.resolve('./polyfills'),
require.resolve('react-dev-utils/webpackHotDevClient'),
paths.appSrc + "/shared-file-view-spreadsheet.js",
],
sharedFileViewUnknown: [
require.resolve('./polyfills'),
require.resolve('react-dev-utils/webpackHotDevClient'),
paths.appSrc + "/shared-file-view-unknown.js",
],
viewFileText: [ viewFileText: [
require.resolve('./polyfills'), require.resolve('./polyfills'),
require.resolve('react-dev-utils/webpackHotDevClient'), require.resolve('react-dev-utils/webpackHotDevClient'),

View File

@@ -70,6 +70,11 @@ module.exports = {
sharedFileViewImage: [require.resolve('./polyfills'), paths.appSrc + "/shared-file-view-image.js"], sharedFileViewImage: [require.resolve('./polyfills'), paths.appSrc + "/shared-file-view-image.js"],
sharedFileViewVideo: [require.resolve('./polyfills'), paths.appSrc + "/shared-file-view-video.js"], sharedFileViewVideo: [require.resolve('./polyfills'), paths.appSrc + "/shared-file-view-video.js"],
sharedFileViewPDF: [require.resolve('./polyfills'), paths.appSrc + "/shared-file-view-pdf.js"], sharedFileViewPDF: [require.resolve('./polyfills'), paths.appSrc + "/shared-file-view-pdf.js"],
sharedFileViewSVG: [require.resolve('./polyfills'), paths.appSrc + "/shared-file-view-svg.js"],
sharedFileViewAudio: [require.resolve('./polyfills'), paths.appSrc + "/shared-file-view-audio.js"],
sharedFileViewDocument: [require.resolve('./polyfills'), paths.appSrc + "/shared-file-view-document.js"],
sharedFileViewSpreadsheet: [require.resolve('./polyfills'), paths.appSrc + "/shared-file-view-spreadsheet.js"],
sharedFileViewUnknown: [require.resolve('./polyfills'), paths.appSrc + "/shared-file-view-unknown.js"],
viewFileText: [require.resolve('./polyfills'), paths.appSrc + "/view-file-text.js"], viewFileText: [require.resolve('./polyfills'), paths.appSrc + "/view-file-text.js"],
viewFileImage: [require.resolve('./polyfills'), paths.appSrc + "/view-file-image.js"], viewFileImage: [require.resolve('./polyfills'), paths.appSrc + "/view-file-image.js"],
viewFileXmind: [require.resolve('./polyfills'), paths.appSrc + "/view-file-xmind.js"], viewFileXmind: [require.resolve('./polyfills'), paths.appSrc + "/view-file-xmind.js"],

View File

@@ -1,7 +1,12 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { gettext } from '../../utils/constants'; import { gettext } from '../../utils/constants';
const { err } = window.shared.pageOptions; const { err, trafficOverLimit } = window.shared.pageOptions;
const propTypes = {
errorMsg: PropTypes.string
};
class SharedFileViewTip extends React.Component { class SharedFileViewTip extends React.Component {
render() { render() {
@@ -9,16 +14,21 @@ class SharedFileViewTip extends React.Component {
if (err == 'File preview unsupported') { if (err == 'File preview unsupported') {
errorMsg = <p>{gettext('Online view is not applicable to this file format')}</p>; errorMsg = <p>{gettext('Online view is not applicable to this file format')}</p>;
} else { } else {
errorMsg = <p className="error">{err}</p>; errorMsg = <p className="error">{err || this.props.errorMsg}</p>;
} }
return ( return (
<div className="shared-file-view-body"> <div className="shared-file-view-body">
<div className="file-view-tip"> <div className="file-view-tip">
{errorMsg} {errorMsg}
{!trafficOverLimit &&
<a href="?dl=1" className="btn btn-secondary">{gettext('Download')}</a>
}
</div> </div>
</div> </div>
); );
} }
} }
SharedFileViewTip.propTypes = propTypes;
export default SharedFileViewTip; export default SharedFileViewTip;

View File

@@ -45,7 +45,7 @@ class SharedFileView extends React.Component {
} }
componentDidMount() { componentDidMount() {
if (trafficOverLimit == 'True') { if (trafficOverLimit) {
toaster.danger(gettext('File download is disabled: the share link traffic of owner is used up.'), { toaster.danger(gettext('File download is disabled: the share link traffic of owner is used up.'), {
duration: 3 duration: 3
}); });
@@ -76,7 +76,7 @@ class SharedFileView extends React.Component {
onClick={this.handleSaveSharedFileDialog}>{gettext('Save as ...')} onClick={this.handleSaveSharedFileDialog}>{gettext('Save as ...')}
</Button> </Button>
}{' '} }{' '}
{(trafficOverLimit === 'False') && {!trafficOverLimit &&
<Button color="success" className="shared-file-op-btn"> <Button color="success" className="shared-file-op-btn">
<a href="?dl=1">{gettext('Download')}({Utils.bytesToSize(fileSize)})</a> <a href="?dl=1">{gettext('Download')}({Utils.bytesToSize(fileSize)})</a>
</Button> </Button>

View File

@@ -0,0 +1,44 @@
import React from 'react';
import ReactDOM from 'react-dom';
import SharedFileView from './components/shared-file-view/shared-file-view';
import SharedFileViewTip from './components/shared-file-view/shared-file-view-tip';
import AudioPlayer from './components/audio-player';
import './css/audio-file-view.css';
const { rawPath, err } = window.shared.pageOptions;
class SharedFileViewAudio extends React.Component {
render() {
return <SharedFileView content={<FileContent />} />;
}
}
class FileContent extends React.Component {
render() {
if (err) {
return <SharedFileViewTip />;
}
const videoJsOptions = {
autoplay: false,
controls: true,
preload: 'auto',
sources: [{
src: rawPath
}]
};
return (
<div className="shared-file-view-body d-flex">
<div className="flex-1">
<AudioPlayer { ...videoJsOptions } />
</div>
</div>
);
}
}
ReactDOM.render(
<SharedFileViewAudio />,
document.getElementById('wrapper')
);

View File

@@ -0,0 +1,109 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { seafileAPI } from './utils/seafile-api';
import { gettext, mediaUrl} from './utils/constants';
import SharedFileView from './components/shared-file-view/shared-file-view';
import SharedFileViewTip from './components/shared-file-view/shared-file-view-tip';
import Loading from './components/loading';
import PDFViewer from './components/pdf-viewer';
import './css/pdf-file-view.css';
const {
repoID, filePath, err,
commitID, fileType, sharedToken
} = window.shared.pageOptions;
class SharedFileViewDocument extends React.Component {
render() {
return <SharedFileView content={<FileContent />} />;
}
}
class FileContent extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoading: !err,
errorMsg: ''
};
}
componentDidMount() {
if (err) {
return;
}
let queryStatus = () => {
seafileAPI.queryOfficeFileConvertStatus(repoID, commitID, filePath, fileType.toLowerCase(), sharedToken).then((res) => {
const convertStatus = res.data['status'];
switch (convertStatus) {
case 'PROCESSING':
this.setState({
isLoading: true
});
setTimeout(queryStatus, 2000);
break;
case 'ERROR':
this.setState({
isLoading: false,
errorMsg: gettext('Document convertion failed.')
});
break;
case 'DONE':
this.setState({
isLoading: false,
errorMsg: ''
});
let scriptNode = document.createElement('script');
scriptNode.type = 'text/javascript';
scriptNode.src = `${mediaUrl}js/pdf/viewer.js`;
document.body.append(scriptNode);
}
}).catch((error) => {
if (error.response) {
this.setState({
isLoading: false,
errorMsg: gettext('Document convertion failed.')
});
} else {
this.setState({
isLoading: false,
errorMsg: gettext('Please check the network.')
});
}
});
};
queryStatus();
}
render() {
const { isLoading, errorMsg } = this.state;
if (err) {
return <SharedFileViewTip />;
}
if (isLoading) {
return <Loading />;
}
if (errorMsg) {
return <SharedFileViewTip errorMsg={errorMsg} />;
}
return (
<div className="shared-file-view-body pdf-file-view">
<PDFViewer />
</div>
);
}
}
ReactDOM.render (
<SharedFileViewDocument />,
document.getElementById('wrapper')
);

View File

@@ -0,0 +1,111 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { seafileAPI } from './utils/seafile-api';
import { siteRoot, gettext } from './utils/constants';
import SharedFileView from './components/shared-file-view/shared-file-view';
import SharedFileViewTip from './components/shared-file-view/shared-file-view-tip';
import Loading from './components/loading';
import './css/spreadsheet-file-view.css';
const {
repoID, filePath, err,
commitID, fileType, fileName, sharedToken
} = window.shared.pageOptions;
class SharedFileViewSpreadsheet extends React.Component {
render() {
return (
<SharedFileView content={<FileContent />} />
);
}
}
class FileContent extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoading: !err,
errorMsg: ''
};
}
componentDidMount() {
if (err) {
return;
}
let queryStatus = () => {
seafileAPI.queryOfficeFileConvertStatus(repoID, commitID, filePath, fileType.toLowerCase(), sharedToken).then((res) => {
const convertStatus = res.data['status'];
switch (convertStatus) {
case 'QUEUED':
case 'PROCESSING':
this.setState({
isLoading: true
});
setTimeout(queryStatus, 2000);
break;
case 'ERROR':
this.setState({
isLoading: false,
errorMsg: gettext('Document convertion failed.')
});
break;
case 'DONE':
this.setState({
isLoading: false,
errorMsg: ''
});
}
}).catch((error) => {
if (error.response) {
this.setState({
isLoading: false,
errorMsg: gettext('Document convertion failed.')
});
} else {
this.setState({
isLoading: false,
errorMsg: gettext('Please check the network.')
});
}
});
};
queryStatus();
}
setIframeHeight = (e) => {
const iframe = e.currentTarget;
iframe.height = iframe.contentDocument.body.scrollHeight;
}
render() {
const { isLoading, errorMsg } = this.state;
if (err) {
return <SharedFileViewTip />;
}
if (isLoading) {
return <Loading />;
}
if (errorMsg) {
return <SharedFileViewTip errorMsg={errorMsg} />;
}
return (
<div className="shared-file-view-body spreadsheet-file-view">
<iframe id="spreadsheet-container" title={fileName} src={`${siteRoot}office-convert/static/${repoID}/${commitID}${encodeURIComponent(filePath)}/index.html?token=${sharedToken}`} onLoad={this.setIframeHeight}></iframe>
</div>
);
}
}
ReactDOM.render (
<SharedFileViewSpreadsheet />,
document.getElementById('wrapper')
);

View File

@@ -0,0 +1,35 @@
import React from 'react';
import ReactDOM from 'react-dom';
import SharedFileView from './components/shared-file-view/shared-file-view';
import SharedFileViewTip from './components/shared-file-view/shared-file-view-tip';
import './css/svg-file-view.css';
const { fileName, rawPath, err } = window.shared.pageOptions;
class SharedFileViewSVG extends React.Component {
render() {
return <SharedFileView content={<FileContent />} />;
}
}
class FileContent extends React.Component {
render() {
if (err) {
return <SharedFileViewTip />;
}
return (
<div className="shared-file-view-body d-flex">
<div className="svg-file-view flex-1">
<img src={rawPath} alt={fileName} id="svg-view" />
</div>
</div>
);
}
}
ReactDOM.render(
<SharedFileViewSVG />,
document.getElementById('wrapper')
);

View File

@@ -0,0 +1,25 @@
import React from 'react';
import ReactDOM from 'react-dom';
import SharedFileView from './components/shared-file-view/shared-file-view';
import SharedFileViewTip from './components/shared-file-view/shared-file-view-tip';
const { err } = window.shared.pageOptions;
class SharedFileViewImage extends React.Component {
render() {
return <SharedFileView content={<FileContent />} />;
}
}
class FileContent extends React.Component {
render() {
if (err) {
return <SharedFileViewTip />;
}
}
}
ReactDOM.render(
<SharedFileViewImage />,
document.getElementById('wrapper')
);

View File

@@ -1,150 +1,44 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import Account from './components/common/account'; import SharedFileView from './components/shared-file-view/shared-file-view';
import { gettext, siteRoot, mediaUrl, logoPath, logoWidth, logoHeight, siteTitle } from './utils/constants'; import SharedFileViewTip from './components/shared-file-view/shared-file-view-tip';
import { Button } from 'reactstrap';
import { Utils } from './utils/utils';
import SaveSharedFileDialog from './components/dialog/save-shared-file-dialog';
import toaster from './components/toast';
import VideoPlayer from './components/video-player'; import VideoPlayer from './components/video-player';
import watermark from 'watermark-dom';
import './css/shared-file-view.css';
import './css/video-file-view.css'; import './css/video-file-view.css';
let loginUser = window.app.pageOptions.name; const { rawPath, err } = window.shared.pageOptions;
const { repoID, sharedToken, trafficOverLimit, fileName, fileSize, rawPath, sharedBy, siteName, enableWatermark, download, err } = window.shared.pageOptions;
class SharedFileViewVideo extends React.Component {
constructor(props) {
super(props);
this.state = {
showSaveSharedFileDialog: false
};
}
handleSaveSharedFileDialog = () => {
this.setState({
showSaveSharedFileDialog: true
});
}
toggleCancel = () => {
this.setState({
showSaveSharedFileDialog: false
});
}
handleSaveSharedFile = () => {
toaster.success(gettext('Successfully saved'), {
duration: 3
});
}
componentDidMount() {
if (trafficOverLimit == 'True') {
toaster.danger(gettext('File download is disabled: the share link traffic of owner is used up.'), {
duration: 3
});
}
}
getContent() {
if (err) {
let errorMsg;
if (err == 'File preview unsupported') {
errorMsg = <p>{gettext('Online view is not applicable to this file format')}</p>;
} else {
errorMsg = <p className="error">{err}</p>;
}
return (
<div className="shared-file-view-body">
<div className="file-view-tip">
{errorMsg}
</div>
</div>
);
} else {
const videoJsOptions = {
autoplay: false,
controls: true,
preload: 'auto',
sources: [{
src: rawPath
}]
};
return (
<div className="shared-file-view-body d-flex text-center">
<div className="video-file-view flex-1">
<VideoPlayer { ...videoJsOptions } />
</div>
</div>
);
}
}
class SharedFileViewImage extends React.Component {
render() { render() {
return <SharedFileView content={<FileContent />} />;
}
}
class FileContent extends React.Component {
render() {
if (err) {
return <SharedFileViewTip />;
}
const videoJsOptions = {
autoplay: false,
controls: true,
preload: 'auto',
sources: [{
src: rawPath
}]
};
return ( return (
<div className="shared-file-view-md"> <div className="shared-file-view-body d-flex">
<div className="shared-file-view-md-header d-flex"> <div className="flex-1">
<React.Fragment> <VideoPlayer { ...videoJsOptions } />
<a href={siteRoot}>
<img src={mediaUrl + logoPath} height={logoHeight} width={logoWidth} title={siteTitle} alt="logo" />
</a>
</React.Fragment>
{ loginUser && <Account /> }
</div> </div>
<div className="shared-file-view-md-main">
<div className="shared-file-view-head">
<div className="float-left">
<h2 className="ellipsis" title={fileName}>{fileName}</h2>
<p className="share-by ellipsis">{gettext('Shared by:')}{' '}{sharedBy}</p>
</div>
{download &&
<div className="float-right">
{(loginUser && loginUser !== sharedBy) &&
<Button color="secondary" id="save" className="shared-file-op-btn"
onClick={this.handleSaveSharedFileDialog}>{gettext('Save as ...')}
</Button>
}{' '}
{(trafficOverLimit === 'False') &&
<Button color="success" className="shared-file-op-btn">
<a href="?dl=1">{gettext('Download')}({Utils.bytesToSize(fileSize)})</a>
</Button>
}
</div>
}
</div>
{this.getContent()}
</div>
{this.state.showSaveSharedFileDialog &&
<SaveSharedFileDialog
repoID={repoID}
sharedToken={sharedToken}
toggleCancel={this.toggleCancel}
handleSaveSharedFile={this.handleSaveSharedFile}
/>
}
</div> </div>
); );
} }
} }
if (enableWatermark) {
let watermark_txt;
if (loginUser) {
watermark_txt = siteName + ' ' + loginUser;
} else {
watermark_txt = gettext('Anonymous User');
}
watermark.init({
watermark_txt: watermark_txt,
watermark_alpha: 0.075
});
}
ReactDOM.render( ReactDOM.render(
<SharedFileViewVideo />, <SharedFileViewImage />,
document.getElementById('wrapper') document.getElementById('wrapper')
); );

View File

@@ -9,11 +9,22 @@
{% render_bundle 'sharedFileViewText' 'css' %} {% render_bundle 'sharedFileViewText' 'css' %}
{% elif filetype == 'Image' %} {% elif filetype == 'Image' %}
{% render_bundle 'sharedFileViewImage' 'css' %} {% render_bundle 'sharedFileViewImage' 'css' %}
{% elif filetype == 'SVG' %}
{% render_bundle 'sharedFileViewSVG' 'css' %}
{% elif filetype == 'Video' %} {% elif filetype == 'Video' %}
{% render_bundle 'sharedFileViewVideo' 'css' %} {% render_bundle 'sharedFileViewVideo' 'css' %}
{% elif filetype == 'Audio' %}
{% render_bundle 'sharedFileViewAudio' 'css' %}
{% elif filetype == 'PDF' %} {% elif filetype == 'PDF' %}
<link rel="resource" type="application/l10n" href="{{ MEDIA_URL }}js/pdf/locale/locale.properties" /> <link rel="resource" type="application/l10n" href="{{ MEDIA_URL }}js/pdf/locale/locale.properties" />
{% render_bundle 'sharedFileViewPDF' 'css' %} {% render_bundle 'sharedFileViewPDF' 'css' %}
{% elif filetype == 'Document' %}
<link rel="resource" type="application/l10n" href="{{ MEDIA_URL }}js/pdf/locale/locale.properties" />
{% render_bundle 'sharedFileViewDocument' 'css' %}
{% elif filetype == 'SpreadSheet' %}
{% render_bundle 'sharedFileViewSpreadsheet' 'css' %}
{% elif filetype == 'Unknown' %}
{% render_bundle 'sharedFileViewUnknown' 'css' %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}
@@ -22,21 +33,21 @@
window.shared = { window.shared = {
pageOptions: { pageOptions: {
repoID: '{{ repo.id }}', repoID: '{{ repo.id }}',
path: '{{ path|escapejs }}', filePath: '{{ path|escapejs }}',
sharedToken: '{{ shared_token }}', sharedToken: '{{ shared_token }}',
trafficOverLimit: '{{ traffic_over_limit }}', trafficOverLimit: {% if traffic_over_limit %}true{% else %}false{% endif %},
fileName: '{{ file_name|escapejs }}', fileName: '{{ file_name|escapejs }}',
fileSize: '{{ file_size }}', fileSize: {{ file_size }},
rawPath: '{{ raw_path|escapejs }}', rawPath: '{{ raw_path|escapejs }}',
sharedBy: '{{ shared_by|email2nickname }}', sharedBy: '{{ shared_by|email2nickname }}',
siteName: '{{ site_name }}', siteName: '{{ site_name }}',
enableWatermark: '{{ enable_watermark }}' == 'True', enableWatermark: {% if enable_watermark %}true{% else %}false{% endif %},
download: '{{ permissions.can_download }}' == 'True', download: '{{ permissions.can_download }}' == 'True',
fileEncodingList: '{{ file_encoding_list|escapejs }}',
encoding: '{{ encoding }}',
fileContent: '{{ file_content|escapejs }}', fileContent: '{{ file_content|escapejs }}',
err: '{{ err }}', err: {% if err %}'{{ err }}'{% else %}''{% endif %},
fileType: '{{ filetype }}',
fileExt: '{{ fileext }}', fileExt: '{{ fileext }}',
commitID: '{{ current_commit.id }}' || '{{ repo.head_cmmt_id }}'
} }
}; };
</script> </script>
@@ -46,8 +57,12 @@
{% render_bundle 'sharedFileViewText' 'js' %} {% render_bundle 'sharedFileViewText' 'js' %}
{% elif filetype == 'Image' %} {% elif filetype == 'Image' %}
{% render_bundle 'sharedFileViewImage' 'js' %} {% render_bundle 'sharedFileViewImage' 'js' %}
{% elif filetype == 'SVG' %}
{% render_bundle 'sharedFileViewSVG' 'js' %}
{% elif filetype == 'Video' %} {% elif filetype == 'Video' %}
{% render_bundle 'sharedFileViewVideo' 'js' %} {% render_bundle 'sharedFileViewVideo' 'js' %}
{% elif filetype == 'Audio' %}
{% render_bundle 'sharedFileViewAudio' 'js' %}
{% elif filetype == 'PDF' %} {% elif filetype == 'PDF' %}
{% render_bundle 'sharedFileViewPDF' 'js' %} {% render_bundle 'sharedFileViewPDF' 'js' %}
<script type="text/javascript"> <script type="text/javascript">
@@ -56,5 +71,17 @@
</script> </script>
<script type="text/javascript" src="{{MEDIA_URL}}js/pdf/pdf.min.js"></script> <script type="text/javascript" src="{{MEDIA_URL}}js/pdf/pdf.min.js"></script>
<script type="text/javascript" src="{{MEDIA_URL}}js/pdf/viewer.js"></script> <script type="text/javascript" src="{{MEDIA_URL}}js/pdf/viewer.js"></script>
{% elif filetype == 'Document' %}
{% render_bundle 'sharedFileViewDocument' 'js' %}
<script type="text/javascript">
var commit_id = '{{ current_commit.id }}' || '{{ repo.head_cmmt_id }}';
var sf_file_url = '{{ SITE_ROOT }}office-convert/static/{{ repo.id }}/' + commit_id + '{{ path|urlencode }}/fake.pdf?token={{shared_token}}';
var sf_pdfworkerjs_url = '{{MEDIA_URL}}js/pdf/pdf.worker.min.js';
</script>
<script type="text/javascript" src="{{MEDIA_URL}}js/pdf/pdf.min.js"></script>
{% elif filetype == 'SpreadSheet' %}
{% render_bundle 'sharedFileViewSpreadsheet' 'js' %}
{% elif filetype == 'Unknown' %}
{% render_bundle 'sharedFileViewUnknown' 'js' %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@@ -12,7 +12,4 @@
{% block render_bundle %} {% block render_bundle %}
{% render_bundle 'viewFileSpreadsheet' 'js' %} {% render_bundle 'viewFileSpreadsheet' 'js' %}
<script type="text/javascript">
//var commit_id = '{{ current_commit.id }}' || '{{ repo.head_cmmt_id }}';
</script>
{% endblock %} {% endblock %}

View File

@@ -1232,7 +1232,7 @@ def view_shared_file(request, fileshare):
template = 'shared_file_view.html' template = 'shared_file_view.html'
if is_textual_file(file_type=filetype) or filetype in (IMAGE, VIDEO, PDF): if filetype != XMIND:
template = 'shared_file_view_react.html' template = 'shared_file_view_react.html'
return render(request, template, { return render(request, template, {