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:
@@ -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",
|
||||
]
|
||||
},
|
||||
|
||||
|
@@ -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: {
|
||||
|
63
frontend/src/components/file-view/file-info.js
Normal file
63
frontend/src/components/file-view/file-info.js
Normal 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;
|
102
frontend/src/components/file-view/file-toolbar.js
Normal file
102
frontend/src/components/file-view/file-toolbar.js
Normal 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;
|
26
frontend/src/components/file-view/file-view-tip.js
Normal file
26
frontend/src/components/file-view/file-view-tip.js
Normal 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;
|
69
frontend/src/components/icon-button.js
Normal file
69
frontend/src/components/icon-button.js
Normal 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;
|
41
frontend/src/css/file-view.css
Normal file
41
frontend/src/css/file-view.css
Normal 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;
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
@@ -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';
|
||||
|
151
frontend/src/view-file-image.js
Normal file
151
frontend/src/view-file-image.js
Normal 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')
|
||||
);
|
@@ -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 }}',
|
||||
|
48
seahub/templates/image_file_view_react.html
Normal file
48
seahub/templates/image_file_view_react.html
Normal 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 %}
|
@@ -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
|
||||
|
Reference in New Issue
Block a user