1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-01 15:09:14 +00:00

[image file view] rewrote it with react (#2983)

* [image file view] rewrote it with react

* [image file view] modification

* [image file view] modified code indentation & etc.
This commit is contained in:
llj
2019-02-22 18:16:44 +08:00
committed by Daniel Pan
parent 834dd3723c
commit 2a2bd2b783
13 changed files with 536 additions and 1 deletions

View File

@@ -103,6 +103,11 @@ module.exports = {
require.resolve('./polyfills'),
require.resolve('react-dev-utils/webpackHotDevClient'),
paths.appSrc + "/view-file-text.js",
],
viewFileImage: [
require.resolve('./polyfills'),
require.resolve('react-dev-utils/webpackHotDevClient'),
paths.appSrc + "/view-file-image.js",
]
},

View File

@@ -69,6 +69,7 @@ module.exports = {
sharedFileViewText: [require.resolve('./polyfills'), paths.appSrc + "/shared-file-view-text.js"],
sharedFileViewImage: [require.resolve('./polyfills'), paths.appSrc + "/shared-file-view-image.js"],
viewFileText: [require.resolve('./polyfills'), paths.appSrc + "/view-file-text.js"],
viewFileImage: [require.resolve('./polyfills'), paths.appSrc + "/view-file-image.js"],
},
output: {

View File

@@ -0,0 +1,63 @@
import React from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import { isPro, gettext, mediaUrl, siteRoot } from '../../utils/constants';
import InternalLinkDialog from '../dialog/internal-link-dialog';
const propTypes = {
toggleStar: PropTypes.func.isRequired,
isLocked: PropTypes.bool.isRequired,
isStarred: PropTypes.bool.isRequired
};
const { fileName, repoID, filePath,
latestContributor, latestContributorName, lastModificationTime
} = window.app.pageOptions;
class FileInfo extends React.PureComponent {
constructor(props) {
super(props);
}
toggleStar = (e) => {
e.preventDefault();
this.props.toggleStar();
}
render() {
const { isStarred, isLocked } = this.props;
const starredText = isStarred ? gettext('starred') : gettext('unstarred');
const lockedText = gettext('locked');
return (
<div>
<h2 className="file-title d-flex align-items-center">
<span className="file-name">{fileName}</span>
<a className={`file-star ${isStarred ? 'fa' : 'far'} fa-star`}
href="#"
title={starredText}
aria-label={starredText}
onClick={this.toggleStar}>
</a>
<InternalLinkDialog repoID={repoID} path={filePath} />
{(isPro && isLocked) &&
<img className="file-locked-icon" width="16"
src={`${mediaUrl}img/file-locked-32.png`}
alt={lockedText}
title={lockedText}
aria-label={lockedText}
/>
}
</h2>
<div className="last-modification">
<a href={`${siteRoot}profile/${encodeURIComponent(latestContributor)}/`}>{latestContributorName}</a>
<span className="last-modification-time">{moment(lastModificationTime * 1000).format('YYYY-MM-DD HH:mm')}</span>
</div>
</div>
);
}
}
FileInfo.propTypes = propTypes;
export default FileInfo;

View File

@@ -0,0 +1,102 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ButtonGroup } from 'reactstrap';
import IconButton from '../icon-button';
import { gettext, siteRoot } from '../../utils/constants';
import { Utils } from '../../utils/utils';
const propTypes = {
isLocked: PropTypes.bool.isRequired,
lockedByMe: PropTypes.bool.isRequired,
toggleLockFile: PropTypes.func.isRequired,
toggleCommentPanel: PropTypes.func.isRequired
};
const {
canLockUnlockFile,
repoID, repoName, parentDir, filePerm, filePath,
canEditFile, err,
encoding, // for 'edit', not undefined only for some kinds of files
canDownloadFile, enableComment
} = window.app.pageOptions;
class FileToolbar extends React.Component {
render() {
const { isLocked, lockedByMe } = this.props;
let showLockUnlockBtn = false;
let lockUnlockText, lockUnlockIcon;
if (canLockUnlockFile) {
if (!isLocked) {
showLockUnlockBtn = true;
lockUnlockText = gettext('Lock');
lockUnlockIcon = 'fa fa-lock';
} else if (lockedByMe) {
showLockUnlockBtn = true;
lockUnlockText = gettext('Unlock');
lockUnlockIcon = 'fa fa-unlock';
}
}
return (
<div>
<ButtonGroup>
<IconButton
id="open-parent-folder"
icon="fa fa-folder-open"
text={gettext('Open parent folder')}
tag="a"
href={`${siteRoot}library/${repoID}/${Utils.encodePath(repoName + parentDir)}`}
/>
{showLockUnlockBtn && (
<IconButton
id="lock-unlock-file"
icon={lockUnlockIcon}
text={lockUnlockText}
onClick={this.props.toggleLockFile}
/>
)}
{filePerm == 'rw' && (
<IconButton
id="history"
icon="fa fa-history"
text={gettext('History')}
tag="a"
href={`${siteRoot}repo/file_revisions/${repoID}/?p=${encodeURIComponent(filePath)}&referer=${encodeURIComponent(location.href)}`}
/>
)}
{(canEditFile && !err) && (
<IconButton
id="edit"
icon="fa fa-edit"
text={gettext('Edit')}
tag="a"
href={`${siteRoot}repo/${repoID}/file/edit/?p=${encodeURIComponent(filePath)}&file_enc=${encodeURIComponent(encoding)}`}
/>
)}
{canDownloadFile && (
<IconButton
id="download-file"
icon="fa fa-download"
text={gettext('Download')}
tag="a"
href="?dl=1"
/>
)}
{enableComment && (
<IconButton
id="comment"
icon="fa fa-comment"
text={gettext('Comment')}
onClick={this.props.toggleCommentPanel}
/>
)}
</ButtonGroup>
</div>
);
}
}
FileToolbar.propTypes = propTypes;
export default FileToolbar;

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { gettext } from '../../utils/constants';
const { err } = window.app.pageOptions;
class FileViewTip extends React.Component {
render() {
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="file-view-content flex-1">
<div className="file-view-tip">
{errorMsg}
<a href="?dl=1" className="btn btn-secondary">{gettext('Download')}</a>
</div>
</div>
);
}
}
export default FileViewTip;

View File

@@ -0,0 +1,69 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, Tooltip } from 'reactstrap';
const propTypes = {
id: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
onClick: PropTypes.func,
tag: PropTypes.string,
href: PropTypes.string
};
class IconButton extends React.Component {
constructor(props) {
super(props);
this.state = {
tooltipOpen: false
};
}
toggle = () => {
this.setState({
tooltipOpen: !this.state.tooltipOpen
});
}
render() {
const className = 'btn-icon';
const btnContent = (
<React.Fragment>
<span className={this.props.icon}></span>
<Tooltip
toggle={this.toggle}
delay={{show: 0, hide: 0}}
target={this.props.id}
placement='bottom'
isOpen={this.state.tooltipOpen}>
{this.props.text}
</Tooltip>
</React.Fragment>
);
if (this.props.tag && this.props.tag == 'a') {
return (
<Button
id={this.props.id}
className={className}
tag="a"
href={this.props.href}
>
{btnContent}
</Button>
);
} else {
return (
<Button
id={this.props.id}
className={className}
onClick={this.props.onClick}
>
{btnContent}
</Button>
);
}
}
}
IconButton.propTypes = propTypes;
export default IconButton;

View File

@@ -0,0 +1,41 @@
body {
overflow: hidden;
}
#wrapper {
height: 100%;
}
.file-view-header {
padding: 10px 15px;
border-bottom: 1px solid #c9c9c9;
flex-shrink: 0;
}
.file-title {
font-size: 1.2rem;
font-weight: bold;
margin-bottom: 5px;
}
.file-star,
.file-internal-link {
font-size: .875rem;
color: #585858;
margin-left: .5rem;
}
.file-star:hover,
.file-star:focus {
text-decoration: none;
color: inherit;
}
.file-locked-icon {
margin-left: .5rem;
}
.last-modification {
font-size: .8125rem;
}
.last-modification-time {
margin-left: .5rem;
}
.file-view-content {
padding: 30px 0;
background: #f4f4f4;
border-right: 4px solid transparent;
}

View File

@@ -1,3 +1,7 @@
.image-file-view {
position: relative;
text-align: center;
}
.image-file-view:before {
content: ' ';
display: inline-block;
@@ -17,3 +21,25 @@
font-size: 0;
line-height: 0;
}
#img-prev,
#img-next {
position:absolute;
top:48%;
text-decoration:none;
color:#888;
width:50px;
height:50px;
background:#fff;
border-radius:100%;
line-height:50px;
}
#img-prev {
left:15px;
}
#img-next {
right:15px;
}
#img-prev:hover,
#img-next:hover {
color:#333;
}

View File

@@ -7,6 +7,7 @@ export const avatarInfo = window.app.config.avatarInfo;
export const logoPath = window.app.config.logoPath;
export const mediaUrl = window.app.config.mediaUrl;
export const siteTitle = window.app.config.siteTitle;
export const siteName = window.app.config.siteName;
export const logoWidth = window.app.config.logoWidth;
export const logoHeight = window.app.config.logoHeight;
export const isPro = window.app.config.isPro === 'True';

View File

@@ -0,0 +1,151 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { seafileAPI } from './utils/seafile-api';
import { Utils } from './utils/utils';
import { gettext, siteRoot, siteName } from './utils/constants';
import FileInfo from './components/file-view/file-info';
import FileToolbar from './components/file-view/file-toolbar';
import FileViewTip from './components/file-view/file-view-tip';
import CommentsList from './components/comments-list';
import watermark from 'watermark-dom';
import './assets/css/fa-solid.css';
import './assets/css/fa-regular.css';
import './assets/css/fontawesome.css';
import './css/file-view.css';
import './css/image-file-view.css';
const { fileName, isStarred, isLocked, lockedByMe,
repoID, filePath, err, enableWatermark, userNickName,
previousImage, nextImage, rawPath // only for image file
} = window.app.pageOptions;
let previousImageUrl, nextImageUrl;
if (previousImage) {
previousImageUrl = `${siteRoot}lib/${repoID}/file${Utils.encodePath(previousImage)}`;
}
if (nextImage) {
nextImageUrl = `${siteRoot}lib/${repoID}/file${Utils.encodePath(nextImage)}`;
}
class ViewFileImage extends React.Component {
constructor(props) {
super(props);
this.state = {
isStarred: isStarred,
isLocked: isLocked,
lockedByMe: lockedByMe,
isCommentPanelOpen: false
};
}
componentDidMount() {
document.addEventListener('keydown', (e) => {
if (previousImage && e.keyCode == 37) { // press '<-'
location.href = previousImageUrl;
}
if (nextImage && e.keyCode == 39) { // press '->'
location.href = nextImageUrl;
}
});
}
toggleCommentPanel = () => {
this.setState({
isCommentPanelOpen: !this.state.isCommentPanelOpen
});
}
toggleStar = () => {
if (this.state.isStarred) {
seafileAPI.unStarFile(repoID, filePath).then((res) => {
this.setState({
isStarred: false
});
});
} else {
seafileAPI.starFile(repoID, filePath).then((res) => {
this.setState({
isStarred: true
});
});
}
}
toggleLockFile = () => {
if (this.state.isLocked) {
seafileAPI.unlockfile(repoID, filePath).then((res) => {
this.setState({
isLocked: false,
lockedByMe: false
});
});
} else {
seafileAPI.lockfile(repoID, filePath).then((res) => {
this.setState({
isLocked: true,
lockedByMe: true
});
});
}
}
render() {
return (
<div className="h-100 d-flex flex-column">
<div className="file-view-header d-flex justify-content-between">
<FileInfo
isStarred={this.state.isStarred}
isLocked={this.state.isLocked}
toggleStar={this.toggleStar}
/>
<FileToolbar
isLocked={this.state.isLocked}
lockedByMe={this.state.lockedByMe}
toggleLockFile={this.toggleLockFile}
toggleCommentPanel={this.toggleCommentPanel}
/>
</div>
<div className="file-view-body flex-auto d-flex">
<FileContent />
{this.state.isCommentPanelOpen &&
<CommentsList toggleCommentsList={this.toggleCommentPanel} />
}
</div>
</div>
);
}
}
class FileContent extends React.Component {
render() {
if (err) {
return <FileViewTip />;
}
return (
<div className="file-view-content flex-1 image-file-view">
{previousImage && (
<a href={previousImageUrl} id="img-prev" title={gettext('you can also press ← ')}><span className="fas fa-chevron-left"></span></a>
)}
{nextImage && (
<a href={nextImageUrl} id="img-next" title={gettext('you can also press →')}><span className="fas fa-chevron-right"></span></a>
)}
<img src={rawPath} alt={fileName} id="image-view" />
</div>
);
}
}
if (enableWatermark) {
watermark.init({
watermark_txt: `${siteName} ${userNickName}`,
watermark_alpha: 0.075
});
}
ReactDOM.render (
<ViewFileImage />,
document.getElementById('wrapper')
);

View File

@@ -31,6 +31,7 @@
logoWidth: '{{ logo_width }}',
logoHeight: '{{ logo_height }}',
siteTitle: '{{ site_title }}',
siteName: '{{ site_name }}',
siteRoot: '{{ SITE_ROOT }}',
loginUrl: '{{ LOGIN_URL }}',
isPro: '{{ is_pro }}',

View File

@@ -0,0 +1,48 @@
{% extends 'base_for_react.html' %}
{% load render_bundle from webpack_loader %}
{% load static seahub_tags i18n %}
{% block sub_title %}{{filename}} - {% endblock %}
{% block extra_script %}
<script type="text/javascript">
// overwrite the one in base_for_react.html
window.app.pageOptions = {
userNickName: '{{request.user.username|email2nickname|escapejs}}',
// for all types of files
fileName: '{{ filename|escapejs }}',
isStarred: {% if is_starred %}true{% else %}false{% endif %},
isLocked: {% if file_locked %}true{% else %}false{% endif %},
latestContributor: '{{ latest_contributor|escapejs }}',
latestContributorName: '{{ latest_contributor|email2nickname|escapejs }}',
lastModificationTime: '{{ last_modified }}',
repoID: '{{ repo.id }}',
repoName: '{{ repo.name|escapejs }}',
filePath: '{{ path|escapejs }}',
filePerm: '{{ file_perm }}',
parentDir: '{{ parent_dir|escapejs }}',
err: '{{ err }}',
lockedByMe: {% if locked_by_me %}true{% else %}false{% endif %},
canLockUnlockFile: {% if can_lock_unlock_file %}true{% else %}false{% endif %},
canEditFile: {% if can_edit_file %}true{% else %}false{% endif %}, // only for some file types
canDownloadFile: {% if can_download_file %}true{% else %}false{% endif %},
enableComment: {% if enable_file_comment %}true{% else %}false{% endif %},
enableWatermark: {% if enable_watermark %}true{% else %}false{% endif %},
// for image file
// img_prev && img_next can be path or None
previousImage: {% if img_prev %}'{{ img_prev|escapejs }}'{% else %}false{% endif %},
nextImage: {% if img_next %}'{{ img_next|escapejs }}'{% else %}false{% endif %},
rawPath: '{{ raw_path|escapejs }}'
};
// for 'comment' panel
window.app.userInfo = {
username: '{{ user.username }}',
name: '{{ user.username|email2nickname|escapejs }}',
contact_email: '{{ user.username|email2contact_email }}',
};
</script>
{% render_bundle 'viewFileImage' %}
{% endblock %}

View File

@@ -685,12 +685,13 @@ def view_lib_file(request, repo_id, path):
return render(request, 'view_file_image.html', return_dict)
elif filetype == IMAGE:
template = 'image_file_view_react.html'
if file_size > FILE_PREVIEW_MAX_SIZE:
error_msg = _(u'File size surpasses %s, can not be opened online.') % \
filesizeformat(FILE_PREVIEW_MAX_SIZE)
return_dict['err'] = error_msg
return render(request, 'view_file_base.html', return_dict)
return render(request, template, return_dict)
img_prev = None
img_next = None