mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-02 23:48:47 +00:00
fix: metadata gallery view toggle detail keep the current visible image (#6964)
Co-authored-by: 杨国璇 <ygx@Hello-word.local>
This commit is contained in:
@@ -7,7 +7,7 @@ import ObjectUtils from '../../metadata/utils/object-utils';
|
|||||||
import { MetadataContext } from '../../metadata';
|
import { MetadataContext } from '../../metadata';
|
||||||
import { PRIVATE_FILE_TYPE } from '../../constants';
|
import { PRIVATE_FILE_TYPE } from '../../constants';
|
||||||
|
|
||||||
const DetailContainer = React.memo(({ repoID, path, dirent, currentRepoInfo, repoTags, fileTags, onClose, onFileTagChanged }) => {
|
const Detail = React.memo(({ repoID, path, dirent, currentRepoInfo, repoTags, fileTags, onClose, onFileTagChanged }) => {
|
||||||
const isView = useMemo(() => path.startsWith('/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES), [path]);
|
const isView = useMemo(() => path.startsWith('/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES), [path]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -61,7 +61,7 @@ const DetailContainer = React.memo(({ repoID, path, dirent, currentRepoInfo, rep
|
|||||||
return !isChanged;
|
return !isChanged;
|
||||||
});
|
});
|
||||||
|
|
||||||
DetailContainer.propTypes = {
|
Detail.propTypes = {
|
||||||
repoID: PropTypes.string,
|
repoID: PropTypes.string,
|
||||||
path: PropTypes.string,
|
path: PropTypes.string,
|
||||||
dirent: PropTypes.object,
|
dirent: PropTypes.object,
|
||||||
@@ -72,4 +72,4 @@ DetailContainer.propTypes = {
|
|||||||
onFileTagChanged: PropTypes.func,
|
onFileTagChanged: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DetailContainer;
|
export default Detail;
|
51
frontend/src/metadata/views/gallery/image.js
Normal file
51
frontend/src/metadata/views/gallery/image.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
const Image = ({
|
||||||
|
isSelected,
|
||||||
|
img,
|
||||||
|
size,
|
||||||
|
onClick,
|
||||||
|
onDoubleClick,
|
||||||
|
onContextMenu,
|
||||||
|
}) => {
|
||||||
|
const [background, setBackground] = useState('#f1f1f1');
|
||||||
|
|
||||||
|
const onLoad = useCallback(() => {
|
||||||
|
setBackground('unset');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id={img.id}
|
||||||
|
tabIndex={1}
|
||||||
|
className={classnames('metadata-gallery-image-item', {
|
||||||
|
'metadata-gallery-image-item-selected': isSelected,
|
||||||
|
})}
|
||||||
|
style={{ width: size, height: size, background }}
|
||||||
|
onClick={onClick}
|
||||||
|
onDoubleClick={onDoubleClick}
|
||||||
|
onContextMenu={onContextMenu}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="metadata-gallery-grid-image"
|
||||||
|
src={img.src}
|
||||||
|
alt={img.name}
|
||||||
|
draggable="false"
|
||||||
|
onLoad={onLoad}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Image.propTypes = {
|
||||||
|
isSelected: PropTypes.bool,
|
||||||
|
img: PropTypes.object,
|
||||||
|
size: PropTypes.number,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
onDoubleClick: PropTypes.func,
|
||||||
|
onContextMenu: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Image;
|
@@ -3,7 +3,7 @@ import { CenteredLoading } from '@seafile/sf-metadata-ui-component';
|
|||||||
import metadataAPI from '../../api';
|
import metadataAPI from '../../api';
|
||||||
import URLDecorator from '../../../utils/url-decorator';
|
import URLDecorator from '../../../utils/url-decorator';
|
||||||
import toaster from '../../../components/toast';
|
import toaster from '../../../components/toast';
|
||||||
import GalleryMain from './gallery-main';
|
import Main from './main';
|
||||||
import ContextMenu from './context-menu';
|
import ContextMenu from './context-menu';
|
||||||
import ImageDialog from '../../../components/dialog/image-dialog';
|
import ImageDialog from '../../../components/dialog/image-dialog';
|
||||||
import ZipDownloadDialog from '../../../components/dialog/zip-download-dialog';
|
import ZipDownloadDialog from '../../../components/dialog/zip-download-dialog';
|
||||||
@@ -32,6 +32,7 @@ const Gallery = () => {
|
|||||||
|
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
const renderMoreTimer = useRef(null);
|
const renderMoreTimer = useRef(null);
|
||||||
|
const lastState = useRef({ visibleAreaFirstImage: { groupIndex: 0, rowIndex: 0 } });
|
||||||
|
|
||||||
const { metadata, store, updateCurrentDirent } = useMetadataView();
|
const { metadata, store, updateCurrentDirent } = useMetadataView();
|
||||||
const repoID = window.sfMetadataContext.getSetting('repoID');
|
const repoID = window.sfMetadataContext.getSetting('repoID');
|
||||||
@@ -97,6 +98,7 @@ const Gallery = () => {
|
|||||||
|
|
||||||
let _groups = [];
|
let _groups = [];
|
||||||
const imageHeight = imageSize + GALLERY_IMAGE_GAP;
|
const imageHeight = imageSize + GALLERY_IMAGE_GAP;
|
||||||
|
const paddingTop = mode === GALLERY_DATE_MODE.ALL ? 0 : DATE_TAG_HEIGHT;
|
||||||
init.forEach((_init, index) => {
|
init.forEach((_init, index) => {
|
||||||
const { children, ...__init } = _init;
|
const { children, ...__init } = _init;
|
||||||
let top = 0;
|
let top = 0;
|
||||||
@@ -108,11 +110,12 @@ const Gallery = () => {
|
|||||||
}
|
}
|
||||||
children.forEach((child, childIndex) => {
|
children.forEach((child, childIndex) => {
|
||||||
const rowIndex = ~~(childIndex / columns);
|
const rowIndex = ~~(childIndex / columns);
|
||||||
if (!rows[rowIndex]) rows[rowIndex] = { top: top + rowIndex * imageHeight, children: [] };
|
if (!rows[rowIndex]) rows[rowIndex] = { top: paddingTop + top + rowIndex * imageHeight, children: [] };
|
||||||
|
child.groupIndex = index;
|
||||||
|
child.rowIndex = rowIndex;
|
||||||
rows[rowIndex].children.push(child);
|
rows[rowIndex].children.push(child);
|
||||||
});
|
});
|
||||||
|
|
||||||
const paddingTop = mode === GALLERY_DATE_MODE.ALL ? 0 : DATE_TAG_HEIGHT;
|
|
||||||
const height = rows.length * imageHeight + paddingTop;
|
const height = rows.length * imageHeight + paddingTop;
|
||||||
_groups.push({
|
_groups.push({
|
||||||
...__init,
|
...__init,
|
||||||
@@ -192,6 +195,22 @@ const Gallery = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!imageSize || imageSize < 0) return;
|
||||||
|
if (imageSize === lastState.current.imageSize) return;
|
||||||
|
const perImageOffset = imageSize - lastState.current.imageSize;
|
||||||
|
const { groupIndex, rowIndex } = lastState.current.visibleAreaFirstImage;
|
||||||
|
const rowOffset = groups.reduce((previousValue, current, currentIndex) => {
|
||||||
|
if (currentIndex < groupIndex) {
|
||||||
|
return previousValue + current.children.length;
|
||||||
|
}
|
||||||
|
return previousValue;
|
||||||
|
}, 0) + rowIndex;
|
||||||
|
const topOffset = rowOffset * perImageOffset + groupIndex * (mode === GALLERY_DATE_MODE.ALL ? 0 : DATE_TAG_HEIGHT);
|
||||||
|
containerRef.current.scrollTop = containerRef.current.scrollTop + topOffset;
|
||||||
|
lastState.current = { ...lastState.current, imageSize };
|
||||||
|
}, [imageSize, groups, mode]);
|
||||||
|
|
||||||
const handleScroll = useCallback(() => {
|
const handleScroll = useCallback(() => {
|
||||||
if (!containerRef.current) return;
|
if (!containerRef.current) return;
|
||||||
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
|
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
|
||||||
@@ -203,11 +222,28 @@ const Gallery = () => {
|
|||||||
const { scrollTop, clientHeight } = containerRef.current;
|
const { scrollTop, clientHeight } = containerRef.current;
|
||||||
const overScanTop = Math.max(0, scrollTop - (imageSize + GALLERY_IMAGE_GAP) * 3);
|
const overScanTop = Math.max(0, scrollTop - (imageSize + GALLERY_IMAGE_GAP) * 3);
|
||||||
const overScanBottom = scrollTop + clientHeight + (imageSize + GALLERY_IMAGE_GAP) * 3;
|
const overScanBottom = scrollTop + clientHeight + (imageSize + GALLERY_IMAGE_GAP) * 3;
|
||||||
|
let groupIndex = 0;
|
||||||
|
let rowIndex = 0;
|
||||||
|
let flag = false;
|
||||||
|
for (let i = 0; i < groups.length; i++) {
|
||||||
|
const group = groups[i];
|
||||||
|
for (let j = 0; j < group.children.length; j++) {
|
||||||
|
const row = group.children[j];
|
||||||
|
if (row.top >= scrollTop) {
|
||||||
|
groupIndex = i;
|
||||||
|
rowIndex = j;
|
||||||
|
flag = true;
|
||||||
|
}
|
||||||
|
if (flag) break;
|
||||||
|
}
|
||||||
|
if (flag) break;
|
||||||
|
}
|
||||||
|
lastState.current = { ...lastState.current, visibleAreaFirstImage: { groupIndex, rowIndex } };
|
||||||
setOverScan({ top: overScanTop, bottom: overScanBottom });
|
setOverScan({ top: overScanTop, bottom: overScanBottom });
|
||||||
renderMoreTimer.current = null;
|
renderMoreTimer.current = null;
|
||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
}, [imageSize, loadMore, renderMoreTimer]);
|
}, [imageSize, loadMore, renderMoreTimer, groups]);
|
||||||
|
|
||||||
const imageItems = useMemo(() => {
|
const imageItems = useMemo(() => {
|
||||||
return groups.flatMap(group => group.children.flatMap(row => row.children));
|
return groups.flatMap(group => group.children.flatMap(row => row.children));
|
||||||
@@ -351,7 +387,7 @@ const Gallery = () => {
|
|||||||
<div className={`sf-metadata-gallery-container sf-metadata-gallery-container-${mode}`} ref={containerRef} onScroll={handleScroll} >
|
<div className={`sf-metadata-gallery-container sf-metadata-gallery-container-${mode}`} ref={containerRef} onScroll={handleScroll} >
|
||||||
{!isFirstLoading && (
|
{!isFirstLoading && (
|
||||||
<>
|
<>
|
||||||
<GalleryMain
|
<Main
|
||||||
groups={groups}
|
groups={groups}
|
||||||
size={imageSize}
|
size={imageSize}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
import React, { useState, useCallback, useMemo, useRef } from 'react';
|
import React, { useState, useCallback, useMemo, useRef } from 'react';
|
||||||
import classnames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import EmptyTip from '../../../components/empty-tip';
|
import EmptyTip from '../../../components/empty-tip';
|
||||||
import { gettext } from '../../../utils/constants';
|
import { gettext } from '../../../utils/constants';
|
||||||
import { GALLERY_DATE_MODE } from '../../constants';
|
import { GALLERY_DATE_MODE } from '../../constants';
|
||||||
|
import Image from './image';
|
||||||
|
|
||||||
const GalleryMain = ({
|
const GalleryMain = ({
|
||||||
groups,
|
groups,
|
||||||
@@ -129,20 +129,15 @@ const GalleryMain = ({
|
|||||||
return row.children.map((img) => {
|
return row.children.map((img) => {
|
||||||
const isSelected = selectedImageIds.includes(img.id);
|
const isSelected = selectedImageIds.includes(img.id);
|
||||||
return (
|
return (
|
||||||
<div
|
<Image
|
||||||
key={img.src}
|
key={img.src}
|
||||||
id={img.id}
|
isSelected={isSelected}
|
||||||
tabIndex={1}
|
img={img}
|
||||||
className={classnames('metadata-gallery-image-item', {
|
size={size}
|
||||||
'metadata-gallery-image-item-selected': isSelected,
|
|
||||||
})}
|
|
||||||
style={{ width: size, height: size, background: '#f1f1f1' }}
|
|
||||||
onClick={(e) => onImageClick(e, img)}
|
onClick={(e) => onImageClick(e, img)}
|
||||||
onDoubleClick={(e) => onImageDoubleClick(e, img)}
|
onDoubleClick={(e) => onImageDoubleClick(e, img)}
|
||||||
onContextMenu={(e) => onImageRightClick(e, img)}
|
onContextMenu={(e) => onImageRightClick(e, img)}
|
||||||
>
|
/>
|
||||||
<img className="metadata-gallery-grid-image" src={img.src} alt={img.name} draggable="false" />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
})}
|
})}
|
@@ -26,7 +26,7 @@ import { MetadataProvider, CollaboratorsProvider } from '../../metadata/hooks';
|
|||||||
import { LIST_MODE, METADATA_MODE, FACE_RECOGNITION_MODE, DIRENT_DETAIL_MODE } from '../../components/dir-view-mode/constants';
|
import { LIST_MODE, METADATA_MODE, FACE_RECOGNITION_MODE, DIRENT_DETAIL_MODE } from '../../components/dir-view-mode/constants';
|
||||||
import CurDirPath from '../../components/cur-dir-path';
|
import CurDirPath from '../../components/cur-dir-path';
|
||||||
import DirTool from '../../components/cur-dir-path/dir-tool';
|
import DirTool from '../../components/cur-dir-path/dir-tool';
|
||||||
import DetailContainer from '../../components/dirent-detail/detail-container';
|
import Detail from '../../components/dirent-detail';
|
||||||
import DirColumnView from '../../components/dir-view-mode/dir-column-view';
|
import DirColumnView from '../../components/dir-view-mode/dir-column-view';
|
||||||
import SelectedDirentsToolbar from '../../components/toolbar/selected-dirents-toolbar';
|
import SelectedDirentsToolbar from '../../components/toolbar/selected-dirents-toolbar';
|
||||||
import { VIEW_TYPE } from '../../metadata/constants';
|
import { VIEW_TYPE } from '../../metadata/constants';
|
||||||
@@ -2427,7 +2427,7 @@ class LibContentView extends React.Component {
|
|||||||
<div className="message err-tip">{gettext('Folder does not exist.')}</div>
|
<div className="message err-tip">{gettext('Folder does not exist.')}</div>
|
||||||
}
|
}
|
||||||
{this.state.isDirentDetailShow && (
|
{this.state.isDirentDetailShow && (
|
||||||
<DetailContainer
|
<Detail
|
||||||
path={this.state.path}
|
path={this.state.path}
|
||||||
repoID={this.props.repoID}
|
repoID={this.props.repoID}
|
||||||
currentRepoInfo={this.state.currentRepoInfo}
|
currentRepoInfo={this.state.currentRepoInfo}
|
||||||
|
Reference in New Issue
Block a user